0%

회사에서 좋은 기회가 생겨 AWS re:invent(2021/11/29 ~ 2021/12/03)에 참석할 기회가 생겼다.
영어도 잘 못하고, 평상시 AWS를 직접 쓰지 않은지 오래 되기도 했지만 견문을 넓히자는 차원에서 지원하여 갔다오게 되었다.
살면서 미국에 처음 가보는 것이다보니 미국에서만 할 수 있는 걸 해보자라는 목표를 세우고 갔으나 많은 실패들이 있었고, 영어가 잘 안되다보니 aws reinvent 컨벤션 후기 보다는 라스베가스 여행기가 되어버린 것 같았다.
기술적인 부분에서 인사이트를 크게 얻지 못해 창피하여 aws reinvent 후기는 적지 못하고, 미국이라는 기회의 땅에 가본 경험을 휘발성 데이터로 냅두기 아까워 기억들이 더이상 날아가기 전에 이렇게라도 기록을 해둬야할 거 같아서 이 글을 쓰게 되었다.
쓰다보니 사진이 많아서인지 글이 좀 루즈해지는 감이 없잖아 있어 파트를 좀 쪼개보았다.

어느정도 익숙해지기 시작한 세미나 둘째 날

베네시안 호텔에 아침부터 키노트가 있어서 이동을 하였다.
그래도 반복되는 길을 이틀 동안 왔다갔다 하다보니 도시의 풍경과 길들이 익숙해지기 시작했다.
또 신기한 것은 라스베가스 호텔 근처에서는 24시간 내내 음악 소리가 들리는 것 같았다.
밤에는 시끄러운 음악이 들렸던 것 같은데 아침에는 또 잔잔한 음악이었나… 여튼 분위기에 맞는 음악이 길거리에 울려퍼지는 게 신기했다.

더 읽어보기 »

회사에서 좋은 기회가 생겨 AWS re:invent(2021/11/29 ~ 2021/12/03)에 참석할 기회가 생겼다.
영어도 잘 못하고, 평상시 AWS를 직접 쓰지 않은지 오래 되기도 했지만 견문을 넓히자는 차원에서 지원하여 갔다오게 되었다.
살면서 미국에 처음 가보는 것이다보니 미국에서만 할 수 있는 걸 해보자라는 목표를 세우고 갔으나 많은 실패들이 있었고, 영어가 잘 안되다보니 aws reinvent 컨벤션 후기 보다는 라스베가스 여행기가 되어버린 것 같았다.
기술적인 부분에서 인사이트를 크게 얻지 못해 창피하여 aws reinvent 후기는 적지 못하고, 미국이라는 기회의 땅에 가본 경험을 휘발성 데이터로 냅두기 아까워 기억들이 더이상 날아가기 전에 이렇게라도 기록을 해둬야할 거 같아서 이 글을 쓰게 되었다.
쓰다보니 사진이 많아서인지 글이 좀 루즈해지는 감이 없잖아 있어 파트를 좀 쪼개보았다.

인천공항에서 라스베가스까지… (11/28)

한국시간 기준 일요일 저녁 출발이었고, 코시국이라 인천공항은 사람이 별로 없었다.
하지만 미국으로 가는 항공편만 사람이 좀 북적여서 수하물을 붙이는데 30분 가량 걸렸다.

더 읽어보기 »

Configuration Metadata

https://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html

Spring Boot jars include metadata files that provide details of all supported configuration properties.
The files are designed to let IDE developers offer contextual help and “code completion” as users are working with application.properties or application.yml files.

Configuration Metadata는 IDE에서 yml 혹은 properties에서 사용하는 Configuration의 자동완성을 도와주는 메타데이터이다. (소스코드에는 영향을 1도 안 미친다.)

더 읽어보기 »

소스코드 외부 세계에서 내부 세계로 데이터를 전달하기 위해서는 미리 정해진 프로토콜 및 API를 통해 데이터를 주고받게 된다.
일반적으로 우리가 많이 사용하는 Restful API(혹은 HTTP API)는 대부분 json의 형태로 데이터를 주고 받게 된다.
그럼 json 문자열이 우리가 정의한 Response 객체로 매핑을 할 때 null을 어떻게 핸들링 해야할까에 집중해서 간단히 정리해보았다.
해당 포스트와 연관성이 높은 (Spring) 외부에서 호출하는 Request 객체를 만들 때 null을 주의하자도 읽는 것을 추천한다.

코틀린

코틀린은 nullable을 지원하다보니 소스코드에서 null에 대한 체크를 매번하지 않아도 돼서 매우 편하다.
하지만 이건 우리 소스코드 내부의 사정이고 소스코드 외부에서 들어오는 데이터의 경우에는 단정지을 수 없다.
그 단적인 예가 네트워크를 통해 들어오는 HTTP API의 응답이다.

1
2
3
4
class ResponseV1(
val number: Int,
val text: String
)

이런 응답 객체가 있다고 할 때 과연 number와 text는 non-null을 100% 보장할 수 있을까??

더 읽어보기 »

소스코드 외부 세계에서 내부 세계로 데이터를 전달하기 위해서는 미리 정해진 프로토콜 및 API를 통해 데이터를 주고받게 된다.
일반적으로 우리가 많이 사용하는 Restful API(혹은 HTTP API)는 대부분 json의 형태로 데이터를 주고 받게 된다.
그럼 json 문자열이 우리가 정의한 Request 객체로 매핑을 할 때 null을 어떻게 핸들링 해야할까에 집중해서 간단히 정리해보았다.
해당 포스트와 연관성이 높은 (Spring) 외부 API의 Response 객체를 만들 때 null을 주의하자도 읽는 것을 추천한다.

코틀린

코틀린은 nullable을 지원하다보니 소스코드에서 null에 대한 체크를 매번하지 않아도 돼서 매우 편하다.
하지만 이건 우리 소스코드 내부의 사정이고 소스코드 외부에서 들어오는 데이터의 경우에는 단정지을 수 없다.
그 단적인 예가 네트워크를 통해 들어오는 HTTP API의 요청이다.

1
2
3
4
5
6
7
8
9
10
class RequestV1(
val number: Int,
val text: String
)

@RestController
class Controller {
@PostMapping
fun api(@RequestBody request: RequestV1) {}
}

이런 요청 객체와 API가 있다고 할 때 과연 number와 text는 non-null을 100% 보장할 수 있을까??

더 읽어보기 »

문제상황

1
2
3
4
5
6
7
8
private Map<String, String> mappings;

public void clear(final CardCompanyCode cardCompanyCode) {
mappings.entrySet().forEach(e -> {
if (e.getValue() != cardCompanyCode) return;
mappings.remove(e.getKey());
});
}

맵에서 entrySet(key/value 쌍)을 가져와 forEach 돌면서 특정 조건에 맞으면 맵에서 요소를 삭제했더니 한 번만 요소가 삭제되고나서 ConcurrentModificationException을 던졌다.

여기서 아래와 같은 의문점이 생겼다.

  1. 맵의 요소를 삭제하는 건데 왜 예외를 던질까?
  2. 왜 한 번만 요소 삭제에 성공하는 걸까?
  3. 하나의 쓰레드에서 작업했는데 왜 ConcurrentModificationException을 던진 걸까?
더 읽어보기 »

3줄 요약

  1. @Transaction(readOnly = true)로 설정하면 select 할 당시 엔티티의 스냅샷을 만들지 않는다.
  2. (JPA) Readonly 트랜잭션은 트랜잭션을 시작하지만 flush를 하지 않는다에서 봤다 싶이 트랜잭션이 커밋될 때 flush를 하지 않는다.
  3. flush를 할 필요가 없기 때문에 Dirty Checking을 할 필요가 없고, 그에 따라서 엔티티의 스냅샷도 만들지 않는 것이다.

엔티티 구조

이해를 편하게 돕기 위해 엔티티는 아래와 같은 구조를 가진다.

1
2
3
4
5
6
7
8
9
10
@Entity
@Table(name = "parents")
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long no;

@Column(name = "helloName")
private SomeType someType;
}
더 읽어보기 »

들어가기에 앞서

글을 정리하다 보니 너무 깊게 파고 정리한 거 같아 글이 너무 길어져서 아무도 읽지 않을 것 같아 정리부터 해보겠습니다.

  1. 엔티티 매니저의 persist 메서드는 리턴값이 없기 때문에 원본 객체를 수정하고, merge 메서드는 리턴값이 있기 때문에 새로운 객체를 반환합니다.
  2. JpaRepository.save 호출 시 엔티티의 식별자(@Id, @EmbeddedId 어노테이션이 붙은 컬럼 등등)가 붙은 필드의 타입이 primitive type이 아닐 때는 null이거나 숫자형일 때는 0이면 새로운 엔티티라고 판단하면서 persist 메서드가 호출되고, 그게 아니면 merge 메서드가 호출됩니다.
  3. JPQL 호출 시 FlushMode가 AUTO(하이버네이트 기본 FlushMode)라 하더라도 쿼리 지연 저장소에 JPQL에서 사용하는 테이블과 관련있는 쿼리가 저장돼있지 않다면 flush를 호출하지 않습니다.
  4. JPQL 호출 시 AutoFlushEvent가 발생하면서 flush 이전에 cascade가 먼저 이뤄지는데 이 때는 PersistEvent가 발생하면서 원본 엔티티를 변경합니다.
  5. JpaRepository.save 호출 시 엔티티가 새로운 엔티티가 아니면 MergeEvent가 발생하고, cascade가 발생하는데 이 때 해당 엔티티에 대해 MergeEvent가 또 발생하면서 Transient 상태인 경우에는 원본 엔티티를 카피하고 카피한 객체의 값을 수정하고 연관관계가 맺어진 엔티티에서는 레퍼런스도 카피 객체로 바꿔치기 하고 있습니다.
  6. JpaRepository.save 호출 시 엔티티가 새로운 엔티티가 아니면 MergeEvent가 발생하는데 cascade 이후에 DirtyChecking이나 Flush가 호출되지 않습니다.
  7. 모든 트랜잭션이 끝난 이후에 커밋 이전에 FlushMode가 MANUAL이 아니고, Managed Entity가 존재하면 FlushEvent를 발생시켜서 DirtyChecking 및 Flush를 하게 됩니다.

제목은 엔티티 매니저의 persist와 merge에 대해 개념을 설명할 것처럼 적어놨지만 이해를 돕기 위해, 흥미 유발을 위해 사내에서 겪었던 문제 과정을 서술하겠습니다.

문제 상황

더 읽어보기 »

3줄 요약

  1. OSIV가 꺼져있으면 트랜잭션이 시작될 때 엔티티 매니저가 생성되고, 트랜잭션이 끝날 때 엔티티 매니저를 종료한다.
  2. OSIV가 꺼져있고, 다른 트랜잭션이라면 엔티티 매니저가 공유되지 않기 때문에 엔티티 매니저의 1차 캐시도 서로 공유되지 않는다.
  3. OSIV가 켜져있으면 요청 당 엔티티 매니저는 한 번 생성되고, 뷰 렌더링이 끝날 때까지 엔티티 매니저는 종료되지 않고 트랜잭션이 다르더라도 1차 캐시가 공유된다.

들어가기에 앞서

엔티티 매니저 팩토리는 생성 비용이 비싸서 대부분 어플리케이션 당 하나를 생성하는 편이고, 엔티티 매니저는 생성 비용이 비싸지 않아서 어플리케이션에서 여러 번 생성된다.
하지만 엔티티 매니저는 쓰레드 세이프 하지 않기 때문에, 쓰레드 당 하나를 생성해야할 것 같고 Spring MVC는 리퀘스트 당 하나의 쓰레드가 할당되기 때문에 리퀘스트 당 하나의 엔티티 매니저가 생성될 것만 같은 기분이 든다.
나 또한 그렇게 알고 있었는데 아래 코드를 통해 뭔가 의문이 생겼다.

1
2
3
4
5
6
7
8
9
10
11
interface SomeRepository : JpaRepository<SomeEntity, Long>

@Service
class SomeService(
private val repository: SomeRepository
) {
fun some() {
val someEntity = repository.findById(1L)
val someEntity2 = repository.findById(1L)
}
}
더 읽어보기 »

3줄 요약

  1. @Transaction(readOnly = true)로 설정해도 트랜잭션은 시작된다. (transaction isolation level 보장)
  2. readOnly 트랜잭션도 시작한 트랜잭션을 종료시켜야하기 때문에 커밋도 한다.
  3. readOnly 트랜잭션의 Hibernate Session의 FlushMode는 Manual로 강제하기 때문에 트랜잭션을 커밋하기 전에 flush를 하지 않는다. (readOnly 보장)

@Transaction(readOnly = true)로 설정해도 트랜잭션은 시작된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface SomeEntityRepository extends JpaRepository<Parent, Long> {
@Transactional(readOnly = true)
List<Parent> findByName(String name);
}

@Service
public SomeService {
private final SomeEntityRepository repository;

public SomeService(final SomeEntityRepository repository) {
this.repository = repository;
}

public void test() {
repository.findByName("qwer");
}
}
더 읽어보기 »