정말 오랜만에 블로그를 써보려고 노트북을 열었네요.
올해 초 지금 회사로 이직 후 정신없이 지내다 보니 블로그는 뒷전이 돼버려 반성을 하는 요즘입니다.
이직 후 새로운 기술 스택에 적응을 하며 이것저것 블로그를 하려고 메모를 해뒀으나, 점점 미루다 보니 무려 7개월이 지났고
현재는 뭘 블로그에 올리려 했는지 기억도 나지 않는 게 현실이네요. 🥲
그 와중에 최근에 오픈한 작고 귀여운 프로젝트에서 제가 주도적으로 진행한 어드민 모듈에 JPA 를 적용하며 겪은 내용을 올려볼까 합니다.
우선 현재 저희가 사용하는 어드민 프로젝트는 Front, Backend 가 한 프로젝트로 구성이 되어 있어 빌드, 배포를 함께 하고,
Backend 의 경우 mybatis 와 java 11 구성으로 되어 있습니다.
요즘 대세(?)를 따라 저희도 kotlin + JPA 를 팀 표준으로 생각하고 대부분의 프로젝트가 해당 스택으로 구성되어 있지만,
어드민의 경우는 후순위로 밀려 현재 스펙으로 머물러 있는 것 같습니다.
어드민 특성상 단순한 CRUD 로직이 많기 때문에 입사 후 꽤나? 적응한 JPA 를 하다 가끔 mybatis 로 쿼리를 일일이 작성하는 게 너무 불편하고, 개발 생산성이 떨어진다는 생각을 하게 되었습니다.
그래서 우선 제가 진행하는 이번 프로젝트의 신규 화면부터 JPA 를 사용할 수 있도록 세팅하여 개발을 진행하였습니다.
JPA 세팅
먼저 기존 어드민 Backend 모듈에 JPA 를 세팅을 먼저 진행해야겠죠?
JPA 가 무엇인지? 사용할 때 장단점이 무엇인지? JPA 세팅 관련 정보 등의 내용은 많은 분들이 이미 해두셨기에 제는 실무에서 겪을 수 있는 몇 가지 내용을 공유드리려고 합니다.
그간 학습이나, 이직을 위한 과제 정도에서 단편적으로 JPA 를 사용할 때 build.gradle 에 spring-data-jpa dependency 만 추가하면 따로 세팅이랄게 없었기에, 가벼운 마음으로 바로 구현을 시작했습니다.
하지만, 늘 인생은 생각대로 흘러가지만은 않죠..😭
1. @EnableTransactionManagement
해당 어노테이션은 단일 데이터소스를 사용하는 환경에서는 명시적으로 사용할 필요가 없지만 다중 데이터소스를 접속하는 환경에서는 필요한 어노테이션 입니다.
어드민에서는 다양한 DB 에 붙을 일이 있다보니 해당 어노테이션을 사용하고 있었는데요.
그러다 보니 저희 코드상 아래와 같이 DB 별로 transactionManager 를 설정하고 있었습니다.
@Configuration
@EnableTransactionManagement
public class AppConfig {
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
JPA 적용 후 dirty checking 을 기대하며, 코드 상 entity 객체 값을 변경하고 transaction 종료 시 DB 에 변경된 값이 저장되길 기대하였으나,
DB 상에 값은 변경 되지 않는 현상이 발생하였습니다.
여러 가지 디버깅을 하며 삽질을 하다 발견한 원인이 바로 저 위의 transactionManager 였습니다.
사실 해당 어노테이션을 사용하고 있는지도 몰랐고, mybatis 사용 당시 DataSourceTransactionManager 되어 있다 보니 JPA 에서 구현된 트랜잭션이 정상 동작하지 않는 것이었습니다. 😭
앞서 말씀드린 것과 같이 여러 데이터소스를 사용하다 보니 데이터소스별 패키지를 구분해야 하는 이슈도 있어서 아래와 같이 @EnableJpaRepositories 어노테이션을 명시적으로 사용하고,
transactionManager 를 JpaTransactionManager 로 바꿔서 원하는 결과를 얻을 수 있었습니다.
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = {"com._2carrot84.admin.infrastructure"},
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager"
)
public class AppConfig {
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setEntityManagerFactory(entityManagerFactory().getObject()); // 필수
jpaTransactionManager.setDataSource(dataSource()); // 권장 (명시성)
return jpaTransactionManager;
}
}
해당 상황은 dirty checking 에 한정된 상황이 아닌 save 를 통한 코드도 DB 반영도 되지 않는 상황이였으니, 오해 없으시길 바랍니다.
위와 같은 삽질을 거친 후 이후 작업 시 기대했던 생산성 눈에 띄게 빨라진 것을 느낄 수 있었습니다.
다만, 어드민 특성상 필수적인 다양한 조회 조건과 페이징 처리 등을 QueryDsl 를 이용하여 처음 구현하면서 생각보다 리소스를 많이 투자하는 악효과가 발생하였다는 비하인드가..😭
2. @EnableJpaAuditing
이번에는 이미 많은 분들이 아실 것 같지만, 제가 느끼기엔 실무에 실직적인 도움이 되는 내용이라 적어봅니다.
이미 JPA 를 사용 중인 다른 모듈 코드를 참고하다 보니 아래 어노테이션을 사용하고 있었습니다.
@EnableJpaAuditing
실무에서는 여러 상황을 대비하기 위해 대부분의 테이블에 생성자/수정자, 생성일시/수정일시 컬럼을 생성해서 이슈 대응을 위한 데이터를 남기곤 합니다.
그간 mybatis 를 사용하면 매번 해당 컬럼 코드를 복사 붙혀넣기 하기 바빴죠.
JPA 에서는 해당 어노테이션과 아래 코드와 같이 별도의 공통 entity 클래스를 생성해서 @EntityListeners(value = {AuditingEntityListener.class}) 를 붙혀주면
@Getter
@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class})
public abstract class BaseAuditEntity {
@CreatedBy
protected Long createdBy;
@CreatedDate
@Column(updatable = false, nullable = false)
protected LocalDateTime createdAt;
@LastModifiedBy
protected Long modifiedBy;
@LastModifiedDate
@Column(nullable = false)
protected LocalDateTime modifiedAt;
}
다른 entity 클래스들은 해당 공통 클래스를 상속받는 것만으로 별도 코드나 구현 신경쓸 필요 없이 해당 컬럼들을 관리할 수 있었습니다.
추가로 생성자/수정자 세팅을 위해 아래 interface 를 적절하게 구현하여 세션이나, 내부에서 사용하는 사용자 식별값을 넣어주면 됩니다.
public interface AuditorAware<T> {
Optional<T> getCurrentAuditor();
}
개인적으로 이렇게 생산성을 높힐 수 있는 요소가 있다는게 참 마음에 들었습니다.
마무리
전 직장에서는 회사 분위기나 DynamoDB 와 Elasticsearch 를 사용하는 특성상 JPA 를 적용해볼 기회가 없었지만, 확실히 새로 모듈에 설정하고 실무에 적용하는 경험은 많은 도움과 자극이 된 것 같습니다.
앞으로도 크지 않아도 효율적인 개발 환경과 생산성을 높이기 위해 다양한 시도를 해볼 수 있으면 좋겠네요.
그럼 이만. 🥕👋🏼🖐🏼