본문 바로가기

Memo & Plan

Spring Data JPA - List 타입과 Auditing 그리고 연관 관계

반응형

1. @Convert(converter = JpaListConverter.class) : Collactions Type 변환

지금껏 Entity 작업을 하면서 굉장히 귀찮게 느껴진 부분이 있었다.

항상 List<String> 과 같은 타입을 어떻게 처리할지 애매하게 느껴졌었다.

 

그래서 따로 정말 필요한 경우는 List<String> 대신 별도의 Entity를 생성해서 연관관계를 설정해주었다.

하지만 완전 특정 Entity 하위에 종속되는 List의 경우, 굳이 별도 Entity를 만들고 복잡한 작업을 하기에는 여러모로 아니라고 생각했다.

 

그런 경우는 보통 @ElementCollection을 통해 작업을 해줬었는데..

막상 DB 구조를 생각하며 사용하다보니 의문이 들었다.

어차피 몇 글자 안되는 경우,  Seperator를 포함한 String이나 JSON 형식으로 치환하는 작업을 하는 게 어떨까?

그래서 좀 찾아보니 직접 복잡한 util을 만들 필요없이 @Convert 어노테이션을 이용하면 간단히 구현할 수 있었다.

 

아래의 JpaListConverter.class는 내가 가이드를 참고 삼아 작성한 부분으로 해당 부분을 참고해 class를 구현한 뒤에 Entity의 대상 필드 위에 @Convert(converter = JpaListConverter.class)를 추가해주면 된다..

 

@Converter
@RequiredArgsConstructor
public class JpaListConverter implements AttributeConverter<List<String>, String>{

    private final ObjectMapper mapper;

    @Override
    public String convertToDatabaseColumn(List<String> dataList) {
        try {
            return mapper.writeValueAsString(dataList);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public List<String> convertToEntityAttribute(String data) {
        try {
            return mapper.readValue(data, new TypeReference<List<String>>(){});
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

}

 

부가적으로 ObjectMapper가 멀트스레드 환경에서 안전한가에 대한 글을 이전에 잠깐 본적이 있었는데.

2.8.6 버전까지 있었던 synchronized가 2.8.7버전부터는 사라지고 thead safe하게 개선되었다고 한다.. 참고하자..

 

2. Jpa Auditing

이미 기존에도 사용하고 있었는데, 대충 습관적으로 사용한 것 같아 내용을 정리하는 게 좋을 것 같다.

분명 @EnableJpaAuditing를 써줬던 기억이 있는데. 왜인지 없어도 돌아가서... 왜 되는거지 싶었다..

 

 

찾아보니 스프링부트에 main 메서드 쪽에 있는 @SpringBootApplication을 사용하면 별도로 어노테이션을 명시하지 않아도 된다고 한다.  > 왜인지 갑자기 안된다. JpaRepository가 아니라 Reposetory를 상속 받아서 그런가.. 몇가지 테스트를 다시 진행해야 할 것 같다.

 

이전에 JpaRepository에 @Repository 없어도 되는 것을 알게 되었을 때랑 같은 기분이다.

뭐 이제 좀 덜 지저분해지지 않을까 하는 긍정적인 마음이 더 크다 ㅎㅎ

 

뭐 아무튼 BaseEntity 상속받아 쓰는 건 그대로여서 간단히 정리해보면 아래와 같다.

 

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseEntity {
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;
    
    @CreatedBy
    @Column(updatable = false, length = 36)
    private String createdBy;

    @LastModifiedDate
    @Column(insertable = false)
    private LocalDateTime lastModifiedDate;

    @LastModifiedBy
    @Column(insertable = false, length = 36)
    private String lastModifiedBy;
}

 

@Entity
@Getter @NoArgsConstructor @AllArgsConstructor @Builder
@Table(name = "PFO_V6_ACCOUNT")
public class Account extends BaseEntity {

	@Id @GeneratedValue(strategy = GenerationType.UUID)
	private String id;
	@Column(unique = true, length = 50)
	private String email;
	private String password;
	@Convert(converter = JpaListConverter.class)
	private List<String> authorities;
	private Boolean accountNonExpired;
	private Boolean accountNonLocked;
	private Boolean credentialsNonExpired;
	private Boolean enabled;
}

 

3. Spring Data JPA 연관관계

우선 연관관계에 앞서 사용 시 주의사항에 대해 한 번 더 남기고 가려고 한다.

연관관계에서 양방향으로 매핑을 시도하는 경우, @ToString이나 JSON으로 직렬화를 하는 경우, 순환참조가 발생해서 문제가 생길 수 있다.

쉽게 예를 들어 User 객체에도 Role이 있고, Role 객체에도 List<User>가 있는데, 양방향으로 연관관계를 맺었을 때, 어떻게 할지를 감안하고 처리해 주어야 한다는 이야기다.

 

예를 들어 한쪽 필드에 @JsonIgnore를 사용한다거나하는 식으로 말이다.
처리하지 않는다면...
User 안에 Role 그 안에 List<User> 안에 User안에 Role 안에 ... List<User> 안에 ...
간단히 생각만해도 문제가 있어보인다.

 

 

다음으로는 연관관계를 꼭 맺어야 하는가에 대한 내용인데,

사실 처음 Spring Data JPA를 배울 때는 연관관계를 바보같이 무조건 다 맺으려고 했다.

mappedBy에 대해 공부하면서 그렇게 하는게 정상인줄 알았다..

 

하지만 좀 더 공부하다 보니 꼭 연관관계를 맺는 것이 필수도 더 좋은 것도 아닐 수 있다는 결론이 내려졌다.

도리어 사용하지도 않을 연관관계를 도배해두면 더 많은 문제가 생길 수도 있다.

 

그래서 알아보니 많은 개발 선배님들께서 가급적 연관매핑은 사용하지 않고 꼭 필요할 때만 추가하는 것을 권장하고 계신 것으로 보였다. (물론 실력이 좋으신 분들은 간단하게 복잡한 연관관계를 잘 컨트롤 하실 수 있겠지만...)

 

양방향 연관관계는 그냥 진짜로 특별한 사유가 있지 않는한 쓰지 않는다고 생각하면 되고 실제로 현업에서 쓸일도 없다고 생각하면 된다. 그나마 단방향 연관관계는 간혹 필요한 경우가 생기는데 그것도 가급적 사용하지 않을 수 있다면 사용하지 않는 것이 좋다고 한다. DB에서도 쓰지도 않을 컬럼 때문에 Join을 하는 행위는 낭비라고 보는 것과도 비슷한 내용이 있다.

 

결론을 내보자면 아래와 같다.

 

1. 조회기능은 별도 모델을 만들어 사용하고(CQRS), 연관객체 탐색이 쉽다는 이유로 연관 매핑을 쓰지 말 것.

2. Embeddable 매핑이 가능하다면 Embeddable 매핑을 사용할 것.

 

보통의 경우, 단순하게 조인을 위한 Key가 되는 값(예: user_id)만 String 방식으로 갖게 해두어도 충분한 것 같다.

 

만약 그럼에도 연관관계 매핑이 필요하다면 아래와 같이하면 된다.

 

1) @OneToOne 단방향 연관관계 매핑 (1:1)

가. @JoinColumn을 통한 연관관계 매핑

@Entity
@Getter @AllArgsConstructor @Builder
@Table(name = "PFO_V6_ORGANIZATION")
public class Organization extends BaseEntity{
	@Id @GeneratedValue(strategy = GenerationType.UUID)
	private String id;
	private String name;
	@OneToOne
	@JoinColumn(name = "account_email")
	private Account owner;
}

 

@JoinColumn 으로 대상 연관관계 대상 Entity 컬럼을 연결해 줄 수 있다. (name으로 DB 컬럼명을 명시해준다.)

참고로 @OneToOne의 Default 전략은 FetchType.EAGER라고 한다.

 

나. @PrimaryKeyJoinColumn을 통한 연관관계 매핑

@Entity
@Getter @NoArgsConstructor
@Table(name = "PFO_V6_ORGANIZATION")
public class AccountRegion extends BaseEntity{
	@Id @Column("account_id")
	private String id;
	private String name;
	@OneToOne
	@PrimaryKeyJoinColumn(name = "account_id")
	private Account account;
    
    @Builder
    public AccountRegion(Account account, String name) {
    	this.id = account.getId();
        this.account = account;
        this.name = name;
    }
}

 

두 개의 Entity 모두 PK 같은 id 값을 사용할 경우, 상위 방식으로 매핑이 가능하다.

 

2) @ManyToOne 단방향 연관관계 매핑 (N:1)

현업에서는 @OneToOne보다 더 잘 사용하지 않는 연관관계 매핑으로 사용하는 방법만 정리해보자면 아래와 같다.

@Entity
@Getter @NoArgsConstructor @AllArgsConstructor @Builder
@Table(name = "PFO_V6_ACCOUNT")
public class Account extends BaseEntity {

	@Id @GeneratedValue(strategy = GenerationType.UUID)
	private String id;
	@Column(unique = true, nullable = false, length = 50)
	private String email;
	private String password;
	@ManyToOne(cascade = CascadeType.ALL)
	@JoinColumn(name = "ORGNIZATION_ID")
	private Organization organization;
}

 

cascade는 임의로 넣어준 값으로 필수는 아니다. FK 사용 시 상위 값이 삭제나 업데이트 되었을 때 어떻게 할 것인지 테스트를 한 것이다. (그냥 DB의 Cascade 설정을 생각하면 된다.)

 

3) @OneToMany 단방향 연관관계 매핑 (1:N)

가. Set

현실에서 해당 구조의 연관관계 매핑을 쓸 일이 얼마나 많을지는 모르겠다. 하지만 쉽게 설명해보자면 Team이 있고 그 안에 List<User> list가 있는 느낌이라고 보면된다. 만약 list에 user가 추가되거나 제거되면 그에 맞게 DB에 반영이 될 것이다. 다만 단방향 연관관계인 만큼 User 쪽에는 Team과 매핑이 되어 있지 않을 것이다. String이나 Long으로 team의 id 값 정도가 들어있지 않을까?

@Entity
@Getter @NoArgsConstructor @AllArgsConstructor @Builder
@Table(name = "PFO_V6_Team")
public class Team extends BaseEntity {

	@Id @GeneratedValue(strategy = GenerationType.UUID)
	private String id;
	@OneToMany
    @JoinColumn(name = "team_id")
    private Set<Account> accounts = new HashSet<>();
}

@Entity
@Getter @NoArgsConstructor @AllArgsConstructor @Builder
@Table(name = "PFO_V6_Account")
public class Account extends BaseEntity {

	@Id @GeneratedValue(strategy = GenerationType.UUID)
	private String id;
	private String email;
}

 

나. List

그럼 순서가 있는 List는 어떨까? 단순하게 생각해서 index no를 컬럼에 함께 넣어줘야한다면 아래 어노테이션을 하나만 추가하면 된다.

@Entity
@Getter @NoArgsConstructor @AllArgsConstructor @Builder
@Table(name = "PFO_V6_Team")
public class Team extends BaseEntity {

	@Id @GeneratedValue(strategy = GenerationType.UUID)
	private String id;
	@OneToMany
    @JoinColumn(name = "team_id")
    @OrderColumn(name = "order_no")
    private List<Account> accounts = new ArrayList<>();
}

@Entity
@Getter @NoArgsConstructor @AllArgsConstructor @Builder
@Table(name = "PFO_V6_Account")
public class Account extends BaseEntity {

	@Id @GeneratedValue(strategy = GenerationType.UUID)
	private String id;
	private String email;
}


다. Map

map의 경우도 비슷하다 순서를 저장할 컬럼 대신 Key 값을 저장할 컬럼이 추가된다고 보면 된다.

@Entity
@Getter @NoArgsConstructor @AllArgsConstructor @Builder
@Table(name = "PFO_V6_Team")
public class Team extends BaseEntity {

	@Id @GeneratedValue(strategy = GenerationType.UUID)
	private String id;
	@OneToMany
    @JoinColumn(name = "team_id")
    @MapKeyColumn(name = "account_email")
    private Map<Account> accounts = new HashMap<>();
}

@Entity
@Getter @NoArgsConstructor @AllArgsConstructor @Builder
@Table(name = "PFO_V6_Account")
public class Account extends BaseEntity {

	@Id @GeneratedValue(strategy = GenerationType.UUID)
	private String id;
	private String email;
}

 

특히나 1:N은 JPA 기술적 제약 때문에 어쩔 수 없이 쓰는 경우를 제외하고 쓸 일이 없다는 점 꼭 생각하고 갔으면 좋겠다.

김영한님, 최범균님 등 많은 분들이 이야기 하셨다고 하니, 만약 자세한 내용이 궁금하다면 여러 블로그와 GPT를 짜집기해 놓은 내 글보다는 저분들의 저서 혹은 영상을 직접 보시는 것이 더 도움이 될 듯하다.

 

물론 국내에서 JPA 책하면 가장 먼저 뜨는 책도 참고해서 공부를 했기 때문에 완전 다른 내용은 아닐 것 같지만...

 

끝으로 혹시라도 제가 틀린 부분이나 더 도움을 주실 내용이 있으신 분들은 말씀 주시면 굉장히 큰 도움이 될 것 같습니다 감사합니다 ㅎㅎ