| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- MVC요청플로우
- Ajax
- 클린아키텍처
- JPA
- 사면초가
- index
- model
- 단위테스트
- 커뮤니티서버
- 로그백
- 공통규약
- JDBC
- 공통기술
- 설정세팅
- SEQUENCE
- Transaction
- 이벤트핸들러등록
- SpringLegacy
- 이벤트핸들러this
- OOP
- springsecurity
- AOP
- DDD
- 문제선택근거
- TDD
- 디스크i/o
- MVC
- 냄새라도
- string
- 전전긍긍
- Today
- Total
개발이군고구마
[전전긍긍 설정세팅] 4. 어노테이션과 BaseEntity/QueryDSL (#공통규약 #공통기술) 본문

JPA에 대해 수박 겉핥기만 해봤으니 BaseEntity 얘기가 나왔을 때, API reseponse와 연관된 건줄 알고 완전 헛다리 짚어...
이불킥 역사를 만들어내었다...ㅎ
이불킥할 시간에 쪽팔림을 자양분 삼아 분석이나 하자. (문득 문득 떠오르는 것도 훌륭한 자양분이다 ^_^)
이참에 JPA 에 관련된 어노테이션, BaseEntity와 QueryDSL까지 말이다.
1. 어노테이션 알고 쓰기
1) Entity
@Getter
@Entity
@NoArgsConstructor (access = AccessLevel.PROTECTED) // JPA 사용시 생성자 필요 조건, 효율성, 접근 제어자
@Table(name = "users") // 테이블 이름을 "users"로 설정
@SQLDelete(sql = "UPDATE users SET isDeleted = true WHERE id = ?")
@Where(clause = "isDeleted = false")
public class User extends BaseEntity {
@Id
@GeneratedValue(generator = "identity")
private Long id;
private String name;
}
@Entity
- 생성자를 추가하는 코드를 쓰지 않아도 자동으로 기본 생성자(@NoArgsConstructor)의 빈생 생성자를 만들어 줌
- 이미 생성자가 있는 경우에는 만들지 않음. * @AllArgsConstructor를 함께 넣어주기만 하면 문제가 될 수 있는 소지
@NoArgsConstructor
- JPA 기본 스펙상 "기본생성자(==빈 생성자)" 를 요구함(없으면 에러)
- Entity 생성 + 그 외 생성자 안 만드는 경우 생략이 가능하지만,
- 괄호 안에 접근 조건을 두고 접근 제한을 걸어 생성자 생성에 대한 보안을 강화 역할
- JPA가 받아들일 수 있는 최대 수준의 생성자가 Protected 이기 때문에 최대한 권한 제어를 할 수 있음
@Builder
- ORM에서는 setter 쓰지 않는다. new 인스턴스 형태로 값을 만들어서 Entity에 매핑시키는 것도 권장되지 않는다고 한다.
- 그렇다면 Builder 패턴으로 사용하는 방법이 있는데 클래스 가장 하위에 멤버별로 전체에 대한 생성자를 요구한다.
- 즉, @AllArgsConstructor 가 없는데 @Builder만 선언되어있으면 컴파일링이 되지 않는다.
@AllArgsConstructor
- 그렇다면 멤버필드에 대한 전체 생성자를 추가하는 어노테이션만 추가해도 무리가 없을 것 같다.
- (하지만 이 방법도 만약 toEntity(DTO) 등에서 Entity를 구성하는 거라면 써주지 않아도 문제가 되지 않는다.
Request
@Getter
@NoArgsConstructor
public class AccountSignUpRequest {
@NotBlank
@Email(message = "이메일을 양식을 지켜주세요.")
private String email;
@NotBlank
private String name;
@NotBlank
private String password;
@Builder
public AccountSignUpRequest(String email, String name, String password) {
this.email = email;
this.name = name;
this.password = password;
}
public Account toEntity() {
return Account.builder()
.email(email)
.name(name)
.password(Password.builder().password(this.password).build())
.build();
}
}
Response
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AccountSignUpResponse {
private String email;
private String name;
@Builder
public AccountSignUpResponse(Account account){
this.email = account.getEmail();
this.name = account.getName();
}
}
@PostMapping("/test/account")
public AccountSignUpResponse signUp(@RequestBody @Valid final AccountSignUpRequest accountSignUpRequest){
// Dto to Entity
Account account = accountService.signUp(accountSignUpRequest.toEntity());
// Entity to Dto
return AccountSignUpResponse.builder().account(account).build();
}
+ 참고

2) 주입
@Autowired
Spring IoC Container 안에 존재하는 해당 Type 의 Bean을 찾아 자동 주입
@Service
public class AService {
@Autowired
private BService bService;
}
⛔ BookRepository 가 bean으로 등록되어 있어야 BookService도 빈으로 등록할 수 있다
@RequiredArgsConstructor
private final 접근제어자로 선언된 멤버변수를 생성자 파라미터로 넣어준다
@Service
@RequiredArgsConstructor
public class AService {
private final BService bService;
}
✅ 권장하는 이유
BService 를 굳이 spring container에 등록하지 않아도 사용이 가능하기 때문
테스트 할 때 용이
2. BaseEntity
JPA를 사용시 @Entity 클래스에서 공통적으로 소유할 가능성이 있는 create_at(intsert time) , update_at(update time) 컬럼들을 따로 분리하여 만든 클래스
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
// 생성일자
@CreatedDate <- 여기서 이 어노테이션 필요하다고 함
@Column(updatable = false)
private LocalDateTime createdDate;
// 수정일자
@LastModifiedDate
private LocalDateTime modifiedDate;
// 생성자 정보 (현재는 Security 설정이 되어있지 않아 주석처리)
// @CreatedBy
private String createdBy;
// 수정자 정보 (현재는 Security 설정이 되어있지 않아 주석처리)
// @LastModifiedBy
private String lastModifiedBy;
// 삭제 여부
private boolean isDeleted = false;
}
@MappedSuperclass
Entity클래스는 Entity클래스끼리만 상속을 받을 수 있는데
Entity클래스에서 일반 클래스를 상속받기 위해서는 해당 어노테이션을 작성해야함
@EntityListeners(AuditingEntityListener.class)
Entity에 LifeCytcle과 관련된 이벤트를 Listen
@Column(updatable = false)
created_at 컬럼은 최초에 값을 insert 할 때 들어가는 값이 유지되어야 하므로 update를 막음
설정 주의
1) 필드에 @CreatedDate, @LastModifiedDate 붙이기
- @CreatedDate : Entity가 생성되어 저장될 때 시간이 자동으로 저장
- @LastModifiedDate : 조회한 Entity의 값을 변경할 때 시간이 자동으로 저장
2) @EnableJpaAuditing을 config 클래스에 추가
@Configuration
//@Entity로 관리되는 JPA 엔터티들의 상태 변화를 감시하는 기능을 킨다는 뜻
@EnableJpaAuditing
public class JPAConfig {
// 여기엔 QueryDSL의 설정 내용이 들어간다
}
soft delete (boolean isDeleted)
데이터를 실제로 삭제하지 않고, 삭제 여부를 나타낼 수 있음
@SQLDelete(sql = "UPDATE users SET is_deleted = true WHERE id = ?")
@Where(clause = "is_deleted = false")
현 사이트에선 이런 기능이 이력테이블과 메인 테이블의 상태값 변경으로 관리되고 있다.
바로 지우지 않고 내역을 가지고 있다는 점에서 같은 맥락인 셈.
delete가 아니라 update로 변경함
지연쓰기가 발생하게 되고, 삭제의 목적을 갖는 update 이기 때문에 바로 select user 시에 정보값이 null 로 반환가능
3. QueryDSL
쿼리를 코드처럼 사용하게 만들어주는 라이브러리
빌더 패턴으로 동적 쿼리를 작성할 수도 있음
1) 설정
variable _super of type com.example.pratice.domain.common.QBaseEntity
-> QueryDSL이 생성한 Q클래스(QBaseEntity, QUser 등)의 소스/빌드 경로를 제대로 설정해주지 않아서
-> Q클래스 자체가 제대로 생성되지 않아 발생하는 문제
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.7'
}
def querydslVersion = "5.0.0"
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation "com.querydsl:querydsl-jpa:${querydslVersion}:jakarta"
annotationProcessor "com.querydsl:querydsl-apt:${querydslVersion}:jakarta"
// ... 이하 생략
}
sourceSets {
main {
java {
srcDirs = ["src/main/java", "build/generated"]
}
}
}
def querydslDir = "$buildDir/generated/querydsl" 로 설정한 폴더에 관련 클래스 생성
2) JPAQueryFactory 생성
@Configuration
//@Entity로 관리되는 JPA 엔터티들의 상태 변화를 감시하는 기능을 킨다는 뜻
@EnableJpaAuditing
public class JPAConfig {
//@PersistenceContext는 EntityManager에 의존성 주입을 담당하는 Annotation이다.
@PersistenceContext
//EntityManager는 JPA에서 엔터티의 생성, 조회, 수정 삭제를 수행하는 객체이다
private EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em){
//쿼리를 작성하는 JPAQueryFactory에 EntityManager를 넘겨 사용한다.
return new JPAQueryFactory(em);
}
}
3) repository 생성
interface를 독립적인 repository로 사용하는 법 vs Spring Data JPA 인터페이스와 함께 사용하는 법이 있는데
굳이 항상 나누어서 개발할 번거로움을 피하기 위해 JPA의 인터페이스에서 함께 사용하는 법을 택한다
a. QueryDslRespository
@Repository
public interface UserQueryDslRepository {
List<User> findAllByName(String name);
}
b. QueryDslRespositoryImpl
@RequiredArgsConstructor
public class UserQueryDslRepositoryImpl implements UserQueryDslRepository {
private final JPAQueryFactory jpaQueryFactory;
@Override
public List<User> findAllByName(String name) {
QUser user = QUser.user;
return jpaQueryFactory
.selectFrom(user)
.where(user.name.eq(name))
.fetch();
}
}
c. 상속
@Repository
public interface UserRepository extends JpaRepository<User, Long>, UserQueryDslRepository {
}
4. 테스트
1) H2 데이터 베이스 연결
Failed to initialize dependency 'dataSourceScriptDatabaseInitializer' of LoadTimeWeaverAware bean 'entityManagerFactory': Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource
테스트를 하기 위해서 테스트 용 임시 Database를 연결해두어야 한다.
2) test 환경에서 실행
@ActiveProfiles("test") 로 Test 클래스를 만들었기 때문에 H2 관련한 설정은 application-test.yml 파일을 생성
testImplementation 'com.h2database:h2'
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
sql:
init:
mode: never
3) @DataJpaTest
SpringBoot 의 웹 서버 없이 테스트를 하는 것이기 때문에 확인은 테스트 코드로 직접 작성하여 확인함
4) 코드
JPA repository에서 함께 상속을 받았기 때문에 JPA 인터페이스만 상속 받으면 된다
@Autowired
UserRepository userRepository;
@DisplayName("QueryDSL 확인용 테스트")
@Test
void 쿼리DSL_테스트() {
// given
User user = new User("testUser");
// JPA 제공 repository 사용
userRepository.save(user);
// when
// QueryDSL을 해당 사용자 조회
List<User> users = userRepository.findAllByName("testUser"); // QueryDSL 사용
users.forEach(u -> System.out.println("#### USER: " + u));
// then
assertThat(users).isNotEmpty();
assertThat(users.get(0).getName()).isEqualTo("testUser");
}
5) 결과
Hibernate: select next value for identity
Hibernate: insert into users (created_by,created_date,is_deleted,last_modified_by,modified_date,name,id) values (?,?,?,?,?,?,?)
Hibernate: select u1_0.id,u1_0.created_by,u1_0.created_date,u1_0.is_deleted,u1_0.last_modified_by,u1_0.modified_date,u1_0.name from users u1_0 where (u1_0.is_deleted = false) and u1_0.name=?
#### USER: com.example.pratice.domain.member.entity.User@5bc1903
참고
https://akku-dev.tistory.com/117 (query dsl)
https://velog.io/@martindog/Spring-JPAQueryFactory-%EC%9D%B4%EC%9A%A9%ED%95%98%EA%B8%B0 (query dsl)
'SERVER > Architecture' 카테고리의 다른 글
| [전전긍긍 설정세팅] 6. 로그 설정 (#공통규약) (0) | 2025.05.28 |
|---|---|
| [전전긍긍 설정세팅] 5. Profile별 설정 과 보안 파일 관리 (#공통규약) (0) | 2025.05.22 |
| [전전긍긍 설정세팅] 3. 2단계 파일 업로드 (#공통기술) (0) | 2025.05.21 |
| [전전긍긍 설정세팅] 2. Swagger API 명세 (#공통규약) (0) | 2025.05.19 |
| [사면초가 TDD/OOP] 1. 구현 원칙 확립 (나는 나의 역할 만을 담당한다) (5) | 2025.05.19 |