0%

소스코드 외부 세계에서 내부 세계로 데이터를 전달하기 위해서는 미리 정해진 프로토콜 및 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");
}
}
더 읽어보기 »

Netty를 사용하다보면 채널 파이프라인에 여러 이벤트 핸들러를 추가하기 마련이다.
그러다보니 순서가 중요할 때가 있다.

  1. 클라에서 보낸 데이터 중에 헤더를 파싱하고,
  2. 헤더에 따라 바디를 파싱하고,
  3. 바디를 토대로 뭔가를 또 처리해야하고…

이런 식으로 N 개의 이벤트 핸들러를 붙여야하고, 순서가 중요하다보니 어떤 순서대로 실행되는지가 궁금해졌다.

Inbound Event Handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ExampleHandler1 : ChannelInboundHandlerAdapter() {
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
println("1")
ctx.fireChannelRead(msg)
}
}

class ExampleHandler2 : ChannelInboundHandlerAdapter() {
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
println("2")
ctx.fireChannelRead(msg)
}
}

class ExampleHandler3 : ChannelInboundHandlerAdapter() {
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
println("3")
ctx.fireChannelRead(msg)
}
}
더 읽어보기 »

Netty는 왜 자바 표준인 NIO의 ByteBuffer를 사용하지 않는 걸까 이유를 몰랐는데 자바 네트워크 소녀 네티를 보고 이유를 알게되어 정리해봄.
ByteBuffer와 ByteBuf의 세부사항 보다는 ByteBuffer는 어떤 문제점을 가지고 있고, ByteBuf는 그 문제점을 어떻게 해결했는지에 초점을 맞추어 정리함.

ByteBuffer의 문제점

Netty의 ByteBuf는 자바의 ByteBuffer가 가진 문제점들을 해결하기 위해 나왔다.

데이터 쓰기/읽기 인덱스가 분리돼있지 않다

1
2
3
4
5
6
7
val byteBuffer = ByteBuffer.allocate(3) // 3바이트를 담을 수 있는 힙버퍼, 전부 0으로 초기화된다.
println(byteBuffer) // java.nio.HeapByteBuffer[pos=0 lim=3 cap=3]

byteBuffer.put(1)
println(byteBuffer) // java.nio.HeapByteBuffer[pos=1 lim=3 cap=3]

println(byteBuffer.get()) // 0
더 읽어보기 »

이벤트 루프의 개념이 명확하지 않아 자바 네트워크 소녀 네티를 보고 정리해봄.

통상적으로 이벤트 기반 어플리케이션이 이벤트를 처리하는 방식은 아래 두 가지가 존재한다고 함.

이벤트 리스너와 이벤트 처리 쓰레드 방식

브라우저에서 DOM에 클릭 이벤트를 어떻게 핸들링하는지 생각해보면 된다.

1
document.querySelector('body').onclick = e => console.dir(e) 
더 읽어보기 »