2023-01-15-TIL
2023-01-15-TIL
Today I Learned
Spring 트랜잭션 매니저와 @Transactional의 동작 원리, 파일 업로드 API 설계 방식을 정리했다. 객체를 로그로 남기는 문제에서 출발해 JSON 변환, ToStringBuilder, ObjectMapper의 스레드 안전성, 자바 직렬화까지 이어서 찾아봤다.
Transaction Manager
Spring은 트랜잭션 처리를 PlatformTransactionManager 인터페이스로 추상화한다. JDBC는 DataSourceTransactionManager, JPA는 JpaTransactionManager, 분산 트랜잭션은 JtaTransactionManager처럼 기술마다 구현체가 있고, 애플리케이션 코드는 어떤 구현을 쓰든 같은 방식으로 트랜잭션을 다룬다. 트랜잭션 매니저는 시작한 커넥션(또는 EntityManager)을 현재 스레드에 바인딩해 두고, 같은 트랜잭션 안의 Repository들이 그 자원을 공유하게 한다.
이 스레드 바인딩 때문에 새 스레드를 만들어 작업을 넘기면 트랜잭션이 따라가지 않는다. 링크된 DZone 글은 여러 스레드에 걸친 트랜잭션 관리 문제를 다룬다. 선언적 방식 대신 TransactionTemplate으로 코드에서 경계를 직접 정하는 프로그래밍 방식도 있다.
- https://p829911.tistory.com/17
- https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#spring-data-tier
- https://dulajra.medium.com/spring-transaction-management-over-multiple-threads-dzone-java-b36a5bc342e5
- https://dzone.com/articles/spring-transaction-management-over-multiple-thread-1
- https://www.baeldung.com/spring-programmatic-transaction-management
- https://ignite.apache.org/docs/latest/extensions-and-integrations/spring/spring-tx
- https://www.ibm.com/docs/en/was/8.5.5?topic=SSEQTP_8.5.5/com.ibm.websphere.nd.multiplatform.doc/ae/cspr_data_access_tran1.htm
- https://dzone.com/articles/spring-transaction-management
@Transactional
@Transactional은 AOP 프록시로 동작한다. 프록시가 메서드 호출을 가로채 트랜잭션 매니저로 트랜잭션을 시작하고, 정상 종료하면 커밋, 예외가 나면 롤백한다. 기본 롤백 대상은 런타임 예외와 Error이고, 체크 예외는 rollbackFor로 지정해야 롤백된다.
Spring 문서가 강조하는 함정은 자기 호출(self-invocation)이다. 프록시 모드(기본값)에서는 프록시를 거쳐 들어오는 외부 호출만 가로채므로, 같은 객체 안에서 다른 @Transactional 메서드를 호출하면 실제 트랜잭션이 적용되지 않는다. 또 문서는 이 문제가 롤백 시나리오를 테스트하기 전까지 드러나지 않을 수 있다고 경고한다.
- https://reiphiel.tistory.com/entry/understanding-of-spring-transaction-management-practice
- https://www.tutorialspoint.com/spring/spring_transaction_management.htm
- https://www.baeldung.com/transaction-configuration-with-jpa-and-spring
- https://medium.com/javarevisited/transaction-management-in-spring-boot-eb01e20b21fe
- https://data-make.tistory.com/738
- https://www.marcobehler.com/guides/spring-transaction-management-transactional-in-depth
File Upload API Design
Tyk의 가이드는 파일 업로드 API를 세 방식으로 정리한다.
| 방식 | HTTP 메서드 | 쓰임 |
|---|---|---|
| 직접 업로드 | PUT | 파일 하나를 바이너리 본문으로 보낸다. Content-Type으로 형식을 알린다 |
| 멀티파트 요청 | POST | 여러 파일, 또는 파일과 메타데이터를 한 요청에 보낸다 |
| 2단계(메타데이터 + 업로드) | POST 후 PUT | 메타데이터로 리소스를 먼저 만들고, 응답받은 위치로 파일을 올린다 |
같은 글은 클라이언트가 파일 URL만 넘기고 서버가 가져오게 하는 방식이 SSRF(서버 측 요청 위조) 위험이 있다고 지적한다. Spring MVC에서는 멀티파트를 MultipartFile과 @RequestPart로 받는다.
- https://tyk.io/blog/api-design-guidance-file-upload/
- https://climbtheladder.com/10-rest-api-file-upload-best-practices/
- https://i8c.be/blog/2020/04/03/design-of-a-file-upload-api/
- https://www.digitalocean.com/community/tutorials/spring-mvc-file-upload-example-single-multiple-files
- https://data-make.tistory.com/596
- https://docs.spring.io/spring-framework/docs/3.0.0.M4/spring-framework-reference/html/ch15s08.html
AtomicInteger
AtomicInteger는 원자적으로 갱신할 수 있는 int 값이다. Javadoc은 원자적으로 증가하는 카운터 같은 용도에 쓰며 Integer를 대체하는 용도는 아니라고 설명한다. incrementAndGet(), compareAndSet() 같은 메서드는 락 없이 CAS(compare-and-swap) 연산으로 구현되어, 여러 스레드가 동시에 값을 올려도 갱신이 유실되지 않는다. 요청 수 집계, 시퀀스 번호 생성처럼 단일 변수의 동시 갱신이 필요할 때 synchronized보다 가볍게 쓸 수 있다.
- https://stackoverflow.com/questions/4818699/practical-uses-for-atomicinteger
AOP
@AfterThrowing 어드바이스로 특정 포인트컷에서 던져진 예외를 가로채 로깅하거나 다른 예외로 감쌀 수 있다. @AfterThrowing은 예외를 관찰할 뿐 삼키지 못하므로, 예외를 바꾸거나 복구하려면 @Around에서 proceed()를 try-catch로 감싸야 한다. 링크된 글이 두 방식을 각각 예제로 보여 준다.
- https://www.spektor.dev/how-to-access-and-handle-java-exceptions-using-spring-aop/
Convert a Java Object into JSON String
lombok의 @ToString 애너테이션에 옵션값을 주어서 JSON 형식으로 오버라이딩 할 수 있으면 좋겠는데, 그런 기능은 제공하지 않는 것 같다.
실제로 Lombok 저장소에는 @ToString에 JSON 출력 옵션을 추가해 달라는 기능 요청(#2449)이 있었지만 닫힌 상태다. JSON 문자열이 필요하면 Jackson ObjectMapper.writeValueAsString()이나 Gson을 쓰는 것이 정석이다. toString()은 디버깅용 표현이라 형식이 바뀌어도 문제가 없어야 하고, 반대로 JSON은 외부와 주고받는 계약이므로 두 역할을 분리하는 편이 낫다.
- https://www.tabnine.com/blog/how-to-convert-a-java-object-into-a-json-string/
- https://stackoverflow.com/questions/24102137/json-object-from-java-objectname-tostring
- https://knunu.tistory.com/12
- https://www.baeldung.com/lombok-tostring
- https://github.com/projectlombok/lombok/issues/2449
- https://github.com/projectlombok/lombok/issues/2248
- https://stackoverflow.com/questions/63650823/how-to-convert-to-object-from-a-string-generated-by-tostring-lombok-annotation
- https://projectlombok.org/features/ToString
- https://www.tabnine.com/code/java/classes/lombok.ToString
- https://zditect.com/blog/55321791.html
ToStringBuilder
Apache Commons Lang의 ToStringBuilder는 toString() 구현을 도와주는 유틸리티다. 출력 형식은 ToStringStyle로 정하는데, 3.4부터 ToStringStyle.JSON_STYLE이 추가되어 JSON 비슷한 형식으로 출력할 수 있다. 의존성을 하나 더 들이는 대신 Lombok 없이도 형식을 바꿀 수 있다는 점이 위 고민에 대한 하나의 답이다.
1
2
3
4
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
}
- https://www.infoworld.com/article/2072464/apache-commons-tostringbuilder.html?page=2
- https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/builder/ToStringBuilder.html
ObjectMapper is Thread-Safe?
Jackson ObjectMapper Javadoc은 모든 설정이 첫 읽기나 쓰기 호출 전에 끝난다면 인스턴스가 완전히 스레드 안전하다고 설명한다. 첫 사용 이후에 설정을 바꾸면 반영이 보장되지 않고 설정 호출 자체가 실패할 수도 있다. 그래서 설정을 마친 ObjectMapper 하나를 static 필드나 Spring 빈으로 공유하는 것이 일반적이다. 요청마다 설정이 달라야 하면 ObjectReader, ObjectWriter를 만들어 쓴다.
- https://velog.io/@ililil9482/ObjectMapper-%EC%82%AC%EC%9A%A9%EB%B2%95
- https://github.com/deeplearning4j/deeplearning4j/issues/2170
- https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/ObjectMapper.html
- https://joyykim.tistory.com/21
- https://stackoverflow.com/questions/3907929/should-i-declare-jacksons-objectmapper-as-a-static-field
- https://stackoverflow.com/questions/63600368/difference-between-jackson-objectmapper-to-others
Java Mapping Frameworks
엔티티와 DTO 사이의 변환을 돕는 매핑 라이브러리는 동작 방식에 따라 성능 차이가 크다. MapStruct처럼 컴파일 시점에 매핑 코드를 생성하는 방식은 직접 작성한 코드와 비슷하게 빠르고, ModelMapper처럼 런타임에 리플렉션으로 필드를 맞추는 방식은 편하지만 느리고 매핑 오류가 실행 중에야 드러난다. Baeldung의 벤치마크 글이 여러 프레임워크를 같은 조건에서 비교한다.
- https://www.baeldung.com/java-performance-mapping-frameworks
- https://better-dev.netlify.app/java/2020/10/26/compare_objectmapper/
JVM Serialization
Java 직렬화 명세에 따르면 클래스의 기본 직렬화 대상은 transient도 static도 아닌 필드다. 이 기본값을 바꾸고 싶으면 private static final ObjectStreamField[] serialPersistentFields를 선언해 직렬화할 필드의 이름과 타입을 명시할 수 있다. 직렬화가 필요한 이유로는 객체를 파일에 저장하거나, 네트워크로 다른 JVM에 전달하거나, 세션을 복제하는 경우가 꼽힌다. ActiveJ Serializer처럼 바이트코드 생성으로 빠른 바이너리 직렬화를 제공하는 라이브러리도 있다.
- https://docs.oracle.com/javase/10/docs/specs/serialization/serial-arch.html#defining-serializable-fields-for-a-class
- https://stackoverflow.com/questions/2475448/what-is-the-need-of-serialization-of-objects-in-java
- https://www.digitalocean.com/community/tutorials/serialization-in-java
- https://www.codejava.net/java-se/file-io/why-do-we-need-serialization-in-java
- https://www.quora.com/Why-is-serialization-required-in-Java
- https://activej.io/serializer
댓글
아직 댓글이 없습니다