네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'2022/04'에 해당되는 글 17건


반응형

JPA를 사용하다 보면 N+1의 문제에 마주치고 Fetch Join을 접하게 된다.

일단 Join으로 N+1을 해결하지 못하는지와 Fetch Join과 일반 Join의 차이점을 정리해보자.

 

Join, Fetch Join 차이점

[ 일반 Join ]

- Fetch Join과 달리 연관 Entity에 Join을 걸어도 실제 쿼리에서 SELECT 하는 Entity는 오직 JPQL에서 조회하는 주체가 되는 Entity만 조회하여 영속화

- 조회의 주체가 되는 Entity만 SELECT 해서 영속화하기 때문에 데이터는 필요하지 않지만 연관 Entity가 검색 조건에는 필요한 경우에 주로 사용한다.

[ Fetch Join ]

- 조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화

- Fetch Join이 걸린 Entity 모두 영속화하기 때문에 FetchType이 LAZY인 Entity를 참조하더라도 이미 영속성 컨텍스트 안에 들어있기 때문에 따로 쿼리가 실행되지 않은 채로 N+1 문제가 해결됨

 


확인 

Team.java

@Entity
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@ToString
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    @Builder.Default
    private List<Member> members = new ArrayList<>();
    
    public void addMember(Member member) {
        member.setTeam(this);
        members.add(member);
    }
}

Member.java

@Entity
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@ToString
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private int age;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}

 

일반 Join을 이용

// TeamRepository.java
@Query("SELECT distinct t FROM Team t JOIN t.members")
public List<Team> findAllWithMembersUsingJoin();
Hibernate:
    select
         distinct team0_.id as id1_1_,
         team0_.name as name2_1_
    from team team0_ 
    inner join
        member members1_
            on team0_.id=members1_.team_id

Team과 Member가 Join 된 형태의 쿼리가 실행됩니다. 하지만 가져오는 컬럼들은 Team의 컬럼인 id와 name만 가져오고 있다.

쿼리를 보면 분명 Join을 했는데 각 Team의 Lazy Entity인 members가 아직 초기화되지 않았다. 

실제로 일반 Join은 실제 쿼리에 Join을 걸어주기는 하지만 Join 대상에 대한 영속성까지는 관여하지 않는다.

 

Fetch Join을 이용한 N+1 해결

// TeamRepository.java
@Query("SELECT distinct t FROM Team t JOIN FETCH t.members")
public List<Team> findAllWithMembersUsingFetchJoin();
Hibernate:
    select
         distinct team0_.id as id1_1_,
         members1_.id as id1_0_1_,
         team0_.name as name2_1_,
         members1_.age as age2_0_1,
         members1_.name as name3_0_1_,
         members1_.team_id as team_id4_0_1_,
         members1_.team_id as team_id4_0_1_,
         members1_.id as id1_0_1_
    from team team0_ 
    inner join
        member members1_
            on team0_.id=members1_.team_id

일반 Join 과 Join의 형태는 같지만 SELECT 하는 컬럼에서 차이가 있다. 

  • 일반 Join : join 조건을 제외하고 실제 질의하는 대상 Entity에 대한 컬럼만 SELECT
  • Fetch Join : 실제 질의하는 대상 Entity와 Fetch join이 걸려있는 Entity를 포함한 컬럼 함께 SELECT

일반 Join 사용처

무작정 Fetch Join을 사용해서 전부 영속성 컨텍스트에 올려서 쓰기보다는 일반 Join을 적절히 이용하여 필요한 Entity만 영속성 컨텍스트에 올려서 사용하는 것이 좋다.

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

[@Autowired 빈 탐색 전략과 @Qualifier, @Primary]

1. 빈(Bean) 등록과 조회 규칙

[ 빈 등록 ]

Spring은 기본적으로 메서드의 이름을 Bean의 이름으로 사용한다. 하지만 개발자가 직접 빈의 이름을 부여할 수도 있다.

@Bean
public DiscountPolicy fixDiscountPolicy() {
    return new FixDiscountPolicy();
}

@Bean("fixDiscountPolicy")
public DiscountPolicy fixDiscountPolicy() {
    return new FixDiscountPolicy();
}

 

[ 빈 조회 규칙 전략 ] 

@Autowired가 등록된 빈을 찾을 때에는 다음과 같은 매칭 규칙으로 빈을 조회한다.

  1. 주고받고자 하는 타입으로 매칭을 시도한다.
  2. 타입이 여러 개면 필드 또는 파라미터 이름으로 매칭을 시도한다.

 

하지만 빈의 이름이 충돌되어 빈 이름만으로 해결이 불가능한 경우나 빈에 추가 구분자나 우선순위를 부여하고 싶은 경우에 @Qualifier나 @Primary 어노테이션을 이용해 편리하게 해결할 수 있다.

 

추가로 Spring은 @Resource라는 어노테이션도 제공하고 있다. @Resource는 @Autowired와 달리 필드 이름으로 빈을 찾는다.

  • @Autowired: 필드 타입을 기준으로 빈을 찾음
  • @Resource: 필드 이름을 기준으로 빈을 찾음

 

2. @Qualifier와 @Primary

[ @Qualifier - 빈의 Alias(구분자) ]

빈의 이름만으로 부족하고, 추가적인 정보가 필요할 수 있다. 그런 상황에서 @Qualifier 어노테이션을 통해 빈에 추가 구분자를 붙여줄 수 있다.

@Qualifier("mainDiscountPolicy")
@Bean
public DiscountPolicy fixDiscountPolicy() {
    return new FixDiscountPolicy();
}

다음과 같이 빈을 찾고자하는 경우에 @Qualifier 어노테이션을 부여하여 빈을 찾도록 할 수 있다.

public class DiscountServie {

    private final DiscountPolicy discountPolicy;
    
    @Autowired
    public DiscountService(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}
  1. 해당 @Qualifier가 붙은 빈을 조회한다.
  2. @Qualifier가 붙은 빈을 찾지 못하면 필드명 또는 파라미터명으로 매칭을 시도한다. 
  3. 그래도 찾지못하면 NoSuchBeanDefinitionException 이 발생한다.

[ @Primary - 빈의 우선순위 부여 ]

여러 타입의 빈이 존재할 때, 특정 빈을 우선적으로 주입하도록 하고 싶다면 @Primary 어노테이션을 사용할 수 있다. Spring이 타입으로 비을 찾다가 Primary가 붙어있는 빈을 발견하면, 바로 해당 빈을 주입시킨다. 즉, @Primary는 여러 개의 빈들 중에 우선순위를 부여하는 방법이다.

@Primary
@Bean
public DiscountPolicy fixDiscountPolicy() {
    return new FixDiscountPolicy();
}

만약 @Primary와 @Qualifier 모두 등록되어있다면 Qualifier가 우선순위를 갖는다.

 

[ 빈 충돌 발생 ]

빈 등록 시, 충돌이 발생한다면

  •  자동 빈 등록 vs 자동 빈 등록: 빈 이름 중복 에러가 발생한다.
  • 수동 빈 등록 vs 자동 빈 등록: 과거에는 수동 빈이 우선순위였지만 최근에는 에러가 발생한다.
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

필요한 데이터를 저장하기 위해 Map<String, Object>를 사용하는 경우가 있다.

 

Map이 아닌 DTO 클래스를 사용해야 하는 이유

[Map을 사용할 때의 단점]

1. 컴파일 에러를 유발할 수 없음.

2. String 텍스트를 Key로 사용함.

3. 가독성이 떨어짐.

4. 타입캐스팅 비용이 발생함.

5. 불변성을 확보할 수 없음.

 

1. 컴파일 에러를 유발할 수 없음

Map의 Value는 Object 타입이다. 그리고 Object 클래스는 최상위 클래스이기 때문에 어떠한 데이터도 넣을 수 있다. Object를 사용할 때의 문제는 어떠한 데이터도 받아들일 수 있기 때문에 타입체크를 할 수 없다는 것이다. 잘못된 타입을 넣어줘도 컴파일 에러를 유발하지 않는다.

 

2. String 텍스트를 Key로 사용함

단순 String을 사용하는 것은 문제가 발생할 여지가 항상 열려있다는 것이다. 굳이 문제의 가능성을 열어두고 이로 인해 불필요한 시간 낭비의 여지를 줄 필요가 없다. DTO를 이용하면 이러한 문제를 해결할 수 있다.

 

3. 가독성이 떨어짐

Map을 사용하는 구조는 가독성이 떨어진다. 어떠한 데이터를 가지고 있는지를 확인할 때는 Map보다 DTO를 확인하는 것이 직관적이고 좋다. 만약 Map<String, Object>를 본다면, 우리가 받는 Key 값은 무엇이고, Value 값은 무엇이며 어떠한 타입인지를 파악하기가 쉽지않다. 만약 Map안에 또 다른 Map이 들어있다면 이러한 문제는 더욱 심각해진다. 결국 Map으로 작성된 코드를 이해하기 위해서는 불필요한 코드 리딩 시간이 필요하고 생산성이 떨어지게 된다.

 

4. 타입캐스팅 비용이 발생함

Map에 있는 데이터를 꺼내서 사용하기 위해서는 반드시 타입 캐스팅을 해야한다.

String name = (String) map.get("name");

이러한 타입캐스팅은 당연히 컴퓨팅 비용을 필요로 한다. 만약 꺼내야 하는 데이터가 많다면 이러한 비용은 더욱 증가하게 된다. 불필요한 타입 캐스팅 비용을 줄이기 위해서도 DTO를 사용하는 것이 좋다.

 

5. 불변성을 확보할 수 없음

Map을 사용하면 해당 데이터의 불변성을 확보할 수 없다. 만약 누군가가 실수로 put 코드를 추가하였다면 기존의 데이터는 없어지고 잘못된 데이터로 덮어 씌워진다.

Map<String, Object> map = new HashMap<>();
map.put("name", "jiyoon");

// 불변성을 확보할 수 없고 값이 변경될 수 있음.
map.put("name", "nero");

Map을 사용하면 불변성을 확보할 수 없으니 DTO를 사용하는 것이 좋다.

 

 

 

Map을 사용하면 위와 같은 단점들을 안게 된다. 이러한 이유로 DTO(Data Transfer Object)라는 데이터 전달 클래스를 사용하는 것이 좋다. DTO를 사용하면 추가적으로 정적 팩토리 메소드를 구현하여 많은 이점을 얻을 수도 있고, 빌더 패턴도 적용할 수 있어 상당히 유용하다. 실제로 업무를 하다보면 이러한 단점을 더욱 잘 체감할 수 있다.

물론 개인적으로 매우 제한적인 경우에 Map을 사용하기도 하는데, 현재도 그렇고 미래에도 절대적으로 단일 Key값을 갖는 케이스라면 컨트롤러에서 Collections.singletonMap로 응답을 반환하기도 한다. 그 외에도 Map을 사용해서 위에서 얘기한 단점들이 부각되지 않거나 유지보수성을 떨어뜨리지 않는다면 Map을 사용해서 오히려 좋아지는 케이스도 있다. 그렇기 때문에 상황을 고려해보았을 때, Map을 사용하여도 위에 적힌 단점들이 부각되지 않거나 오히려 이점을 얻을 수 있는 경우에는 Map을 사용해도 괜찮다. 하지만 위에서 얘기했던 단점들이 드러나는 상황이라면 Map보다는 DTO를 이용하는 것을 권장한다.



 출처 

https://mangkyu.tistory.com/164

 

[Java] Map보다 DTO 클래스를 사용해야 하는 이유

필요한 데이터를 저장하기 위해 Map 를 사용하는 개발자들이 있습니다. 하지만 Map을 사용하면 너무 많은 단점들을 안게 되는 것 같아서, 왜 Map이 아닌 DTO 클래스를 사용해야 하는지에 대해 정리

mangkyu.tistory.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

개발을 하다보면 API의 요청이나 응답을 처리할 때 또는 다른 계층으로 넘기는 파라미터가 너무 많은 시점에 별도의 DTO를 생성하는 것이 좋습니다.

 

[엔티티(Entity) 또는 도메인 객체(Domain Object)와 DTO를 분리해야 하는 이유]

1) 관심사의 분리

2) Validation 로직 및 불필요한 코드 등과의 분리

3) API 스펙과의 분리

4) API 스펙의 파악이 용이

 

1. 관심사의 분리

엔티티와 DTO를 분리해야 하는 가장 근본적인 이유는 관심사가 서로 다르기 때문이다. 관심사의 분리(separation of concerns, SoC)는 소프트웨어 분야의 오래된 원칙 중 하나로서, 서로 다른 관심사들을 분리하여 변경 가능성을 최소화하고, 유연하며 확장가능한 클린 아키텍처를 구축하도록 도와준다.

DTO(Data Transfer Object)의 핵심 관심사는 이름 그대로 데이터의 전달이다. DTO는 데이터를 담고, 다른 계층(Layer) 또는 다른 컴포넌트들로 데이터를 넘겨주기 위한 자료구조(Data Structure)이다. 그러므로 어떠한 기능 및 동작도 없어야 한다.

반면에 엔티티는 핵심 비즈니스 로직을 담는 비즈니스 도메인 영역의 일부이다. 그러므로 엔티티 또는 도메인 객체는 그에 따른 비즈니스 로직이 추가될 수 있다. 엔티티 또는 도메인 객체는 다른 계층이나 컴포넌트 사이에서 전달을 위해 사용되는 객체가 아니다.

엔티티와 DTO는 엄연히 서로 다른 관심사를 가지고 있고, 그렇기 때문에 분리하는 것이 합리적이다.

 

2. Validation 로직 및 불필요한 코드 등과의 분리

Spring 에서는 요청에 대한 데이터를 검증하기 위해 @Valid 어노테이션을 지원하고 있다.

@Valid 처리를 위해서는 @NotNull, @NotEmpty, @Size 등과 같은 유효성 검증 어노테이션들을 변수에 붙여주어야 한다. 반면에 JPA도 변수에 @Id, @Column 등과 같은 어노테이션을 활용해 객체와 관계형 데이터베이스를 매핑해주는데, DTO와 엔티티를 분리하지 않는다면 엔티티의 코드가 상당히 복잡해진다. 

또한, 엔티티 클래스의 생성일자, 수정일자를 나타내는 변수들은 API 요청이나 응답에서는 필요가 없다. 그렇기 때문에 응답에서 해당 변수를 제거하기 위해서는 @JsonIgnore 등과 같은 또 다른 어노테이션을 통해 별도의 작업이 필요할 수도 있다.

이렇게 엔티티 클래스를 사용하게 되면 핵심 비즈니스 도메인 코드들이 아닌 요청/응답을 위한 값, 유효성 검증을 위한 코드 등이 추가되면서 엔티티 클래스가 지나치게 비대해질 것이고, 확장 및 유지보수 등에서 매우 어려움을 겪게 될 것이다. 

그러므로 이러한 경우에는 별도의 DTO를 생성해서 엔티티로부터 분리하는 것이 바람직할 것이다.

 

3. API 스펙의 유지

@Entity 
@Table 
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Membership {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false)
    private Long id;
    
    @Enumerated(EnumType.STRING)
    private MembershipType membershipType; 
    
    @Column(nullable = false) 
    private String userId;
    
    @Setter 
    @Column(nullable = false) 
    @ColumnDefault("0") 
    private Integer point;
}
{ 
    "id" : "15", 
    "membershipType" : "NAVER", 
    "userId" : "NC10523", 
    "point" : "10000" 
}

내부 정책 변경으로 userId를 memberId로 변경해야 하는 상황이다. DTO를 사용하지 않는다면 userId가 memberId로 바꿈에 따라 API 스펙이 변경되고, API를 사용하던 사용자들은 모두 장애를 겪게 될 것이다. 물론 @JsonProperty를 이용해 반환되는 값의 이름을 변경할 수 있지만, 이는 결국 Entity를 무겁게 만들어 근본적인 해결책이 될 수 없다.
스펙이 변경되어 테이블에 컬럼이 추가되는 경우도 마찬가지이다. 테이블에 새로운 컬럼이 추가되면 엔티티에 새로운 변수가 추가될 것이고, 별도로 처리를 하지 않는 이상 API 스펙이 변경되는 것이다.

이러한 상황을 위해 DTO를 이용해 분리하여 독립성을 높임으로써 변경이 전파되는 것을 방지해야 한다. 만약 우리가 응답을 위한 DTO 클래스를 활용하고 있으면, Entity 클래스의 변수가 변경되어도 API 스펙이 변경되지 않으므로 안정성을 확보할 수 있다.

 

4. API 스펙의 파악 용이

DTO를 사용함으로써 얻는 또 다른 장점은 DTO를 통해 API 스펙을 어느정도 파악할 수 있다는 점이다. API 문서의 요약본을 작성하는 것과 유사한 효과를 얻을 수 있으며, 특히 요즘같은 MSA 아키텍처로 개발을 많이 하는 상황에서 다른 사람이 작성한 API 호출 코드를 파악할 때 요청/응답을 손쉽게 파악할 수 있어 용이하다.

 

출처

https://mangkyu.tistory.com/192

 

[Spring] 엔티티(Entity) 또는 도메인 객체(Domain Object)와 DTO를 분리해야 하는 이유

개발을 하다 보면 API의 요청이나 응답을 처리할 때 또는 다른 계청으로 넘기는 파라미터가 너무 많은 시점에 별도의 DTO를 생성해야 하나 고민을 하는 시점이 생깁니다. 개인적으로는 간단한 애

mangkyu.tistory.com

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

nullable한 두 개의 String의 equality 비교를 어떻게 null safe하게 할 수 있을까?

 

java.util.Objects.equals(Object, Object)

Java7부터 java.utils.Objects의 static 메서드 equals(Object, Object)를 사용할 수 있다. 이 메서드는 둘다 null이면 true를, 둘 중 하다가 null이면 false를, 그렇지 않으면 equals의 결과를 리턴한다.

 

nullable한 객체에서 메소드를 호출하는게 아니라 static 메소드이므로 NullPointerException의 발생 가능성이 없고 별도의 라이브러리를 추가할 필요가 없다는 장점이 있다.

public final class Objects {

    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }
}

StringUtils.equals(CharSequence, CharSequence) - Apache Commons

다른 방법으로는 apache commons 라이브러리의 StringUtils가 제공하는 static 메소드 equals()를 사용할 수 있다.

public class StringUtils {

    public static boolean equals(CharSequence cs1, CharSequence cs2) {
        if (cs1 == cs2) {
            return true;
        } else if (cs1 != null && cs2 != null) {
            if (cs1 instanceof String && cs2 instanceof String) {
                return cs1.equals(cs2);
            } else {
                return cs1.length() == cs2.length() && CharSequenceUtils.regionMatches(cs1, false, 0, cs2, 0, cs1.length());
            }
        } else {
            return false;
        }
    }
}

이를 사용하려면 apache commons 라이브러리 의존성을 추가해야한다.

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !