Coding 공부/IntelliJ

[IntelliJ_Springboot_MariaDB] Querydsl 예제코드, Mapper, BoardService 예제코드, BooleanBuilder, @Configuration, @Bean

CBJH 2024. 5. 14.
728x90
반응형

1. Repository 클래스, 인터페이스들간의 관계

  • public interface BoardRepository extends JpaRepository<Board, Long>, BoardSearch{}
  • public interface BoardSearch {}
  • public class BoardSearchImpl extends QuerydslRepositorySupport implements BoardSearch {}
  • JpaRepository 

 

  • Java에서 사용되는 이 코드 조각은 Spring Framework의 일부를 활용한 데이터 접근 계층(DAO, Data Access Object) 구현의 예를 보여줍니다. 각각의 역할과 관계를 간단하게 설명해 드리겠습니다.

1.1 JpaRepository

  • JpaRepository는 Spring Data JPA의 인터페이스로, 데이터베이스와의 상호작용을 쉽게 하기 위해 일반적인 데이터 접근 메소드들(save, findById, findAll, delete 등)을 제공합니다. 이 인터페이스를 확장함으로써 개발자는 특정 엔티티에 대한 CRUD(Create, Read, Update, Delete) 기능을 쉽게 구현할 수 있습니다.

1.2 BoardRepository

  • BoardRepository는 JpaRepository를 상속받는 인터페이스입니다. 여기서 Board는 엔티티 클래스이고, Long은 해당 엔티티의 ID 필드의 타입입니다. 이 인터페이스를 통해 Board 엔티티에 대한 데이터베이스 연산을 수행할 수 있으며, JpaRepository에 정의된 기본 메소드 외에도 필요에 따라 추가적인 메소드를 정의할 수 있습니다.

1.3 BoardSearch

  • BoardSearch는 사용자 정의 인터페이스로, Board 엔티티에 대해 특정한 검색 기능을 구현하기 위해 사용됩니다. 이 인터페이스 자체는 메소드를 정의하지 않고, 구현 클래스에서 이를 구현할 것입니다.

1.4 BoardSearchImpl

  • BoardSearchImpl 클래스는 QuerydslRepositorySupport를 상속받아 BoardSearch 인터페이스를 구현합니다. QuerydslRepositorySupport는 Querydsl 라이브러리를 사용하여 복잡한 쿼리를 구성하고 실행하는데 도움을 주는 클래스입니다. BoardSearchImpl에서는 BoardSearch 인터페이스에 정의된 메소드(여기서는 없으니 추가 필요)를 실제로 구현하여 사용자 정의 쿼리를 실행할 수 있습니다.

1.5 전체적인 관계

  • **BoardRepository**는 기본적인 JPA 기능을 제공하는 JpaRepository와 추가적인 사용자 정의 검색 기능을 제공하는 BoardSearch 인터페이스를 모두 상속받습니다.
  • **BoardSearchImpl**은 BoardSearch의 구현체로서, 필요한 검색 로직을 구현합니다.

 

2. Querydsl 예제코드

public interface BoardSearch {
    Page<Board> search1(Pageable pageable);

    Page<Board> searchAll(String[] types, String keyword, Pageable pageable);
}
  • 클래스 만들기 전 인터페이스 구현
public class BoardSearchImpl extends QuerydslRepositorySupport implements BoardSearch {
    public BoardSearchImpl() {
        super(Board.class); // Querydsl 적용을 위해 Board 엔티티 클래스를 명시
    }

    @Override
    public Page<Board> search1(Pageable pageable) {
        QBoard board = QBoard.board; // Querydsl을 통해 생성된 Board 엔티티의 Q 모델 인스턴스
        JPQLQuery<Board> query = from(board); // JPQLQuery를 시작, Board를 대상으로 함
        query.where(board.title.contains("3")); // 제목에 '3'이 포함된 글 검색
        
        this.getQuerydsl().applyPagination(pageable, query); // 페이지 처리 적용

        List<Board> list = query.fetch(); // 쿼리 결과를 List로 가져옴
        long count = query.fetchCount(); // 총 결과 수를 계산

        // 결과를 PageImpl 객체로 만들어 반환
        return new PageImpl<>(list, pageable, count);
    }

    @Override
    public Page<Board> searchAll(String[] types, String keyword, Pageable pageable){
        QBoard board = QBoard.board; // Querydsl을 통해 생성된 Board 엔티티의 Q 모델 인스턴스
        JPQLQuery<Board> query = from(board); // JPQLQuery 시작

        if((types != null) && (types.length > 0) && keyword != null) {
            BooleanBuilder booleanBuilder = new BooleanBuilder(); // 조건을 동적으로 구성하기 위한 BooleanBuilder 생성
            for(String type : types) {
                switch(type) { // 검색 유형에 따라 조건 추가
                    case "t":
                        booleanBuilder.or(board.title.contains(keyword)); // 제목 검색
                        break;
                    case "c":
                        booleanBuilder.or(board.content.contains(keyword)); // 내용 검색
                        break;
                    case "w":
                        booleanBuilder.or(board.writer.contains(keyword)); // 작성자 검색
                        break;
                }
            }
            query.where(booleanBuilder); // 구성된 조건을 쿼리에 적용
        }
        query.where(board.bno.gt(0L)); // 게시글 번호(bno)가 0보다 큰 것만 검색 (추가 조건)

        this.getQuerydsl().applyPagination(pageable, query); // 페이지 처리 적용
        List<Board> list = query.fetch(); // 쿼리 결과를 List로 가져옴
        Long count = query.fetchCount(); // 총 결과 수 계산

        return new PageImpl<>(list, pageable, count); // 결과를 PageImpl 객체로 만들어 반환
    }
}

 

  • 이전에 만든 BoardSearch 인터페이스를 implement하고, Querydsl을 사용하기 위해 QuerydslRepositorySupport를 상속 받는다.
  • 생성자에서 어떤 클래스를 JPA로 Repository할건지 매개변수로 전달한다.
  • 페이지네이션 설정을 담은 Pageable는 @Test에서 설정하고 매개변수로 받는다. (실무에선 글 목록이나 댓글 목록이나 DB에 저장된 값이 무수히 많기 때문에 페이지네이션을 해서 필요한 정보만 받아와야 처리가 수월하다.)
  • JPQLQuery<Board>는 Querydsl을 사용하기 위한 필수 컬렉션이다. 다양한 JPA Method를 사용 할 수 있다.
  • JPA Method 링크 : https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html
  • 리턴값은 Page<Board>이므로 new PageImpl<>();로 객체 인스턴스를 생성해 반환한다. Page<Board>는 인터페이스이고, PageImpl<Board> 클래스는 이를 구현한다.  이는 업캐스팅이다.
  • seachAll()은 title만 검색할지, content만 검색할지, writer만 검색할지 String type[]; 매개변수로 받아서 Querydsl을 작성한다. 
  • BooleanBuilder는 여러 조건을 AND, OR 등으로 연결하여 전체 쿼리의 WHERE 절을 동적으로 구성한다.(밑에 예제와 함께 자세히 설명)
@Test
public void testSearchAll(){
    String[] types = {"t", "c", "w"};
    String keyword = "3";
    Pageable pageable = PageRequest.of(1, 10, Sort.by("bno").descending());
    Page<Board> result = boardRepository.searchAll(types, keyword, pageable);
    log.info("total count: " + result.getTotalElements());
    log.info("total pages: " + result.getTotalPages());
    log.info("page number: " + result.getNumber());
    log.info("page size: " + result.getSize());
    log.info("result.hasPrevious()+\"__\"+result.hasNext(): "+ result.hasPrevious()+"__"+result.hasNext());//이전 페이지가 있냐?__다음 페이지가 있냐?

    result.getContent().forEach(board -> log.info(board));
}
  • BoardRepositoryTests 클래스 내에 @Test메소드.
  • Pageable에 PageRequest.of();로 설정을 담아 Page<Board>를 반환 받는다.

 

 

3. Mapper 예제코드

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {
    private Long bno;
    private String title;
    private String content;
    private String writer;
    private LocalDateTime regDate;
    private LocalDateTime modDate;
}
  • BoardDTO 클래스
@Configuration
public class RootConfig {
    @Bean
    public ModelMapper getMapper(){
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
                .setFieldMatchingEnabled(true)
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)  //private에 접근할 수 있는 권한 부여
                .setMatchingStrategy(MatchingStrategies.LOOSE); //매칭을 Loose하도록 설정
        return modelMapper;
    }
}
  • ModelMapper를 반환 받는 getMapper() 메소르를 담는 RootConfig 클래스
  • JPA에서 DB에 접근하는 Board Entity와 BoardDTO을 매핑시키기 위해 사용된다.
  • 매핑을 사용하는 이유는, Board를 Repository 패키지를 제외한 외부 클래스에서 접근하지 못하도록 하기 위함이다. Board Entity와 BoardDTO를 분리해서, 프론트에선 BoardDTO를 사용해 객체를 전달한다.
  • 서블릿에서는 Enum 타입으로, 싱글톤으로 사용했으나 스프링에선 @Configuration으로 대체한다.

3.1 @Configuration

@Configuration 어노테이션은 클래스 레벨에서 사용되며, 해당 클래스가 Spring의 구성(Configuration) 클래스임을 나타냅니다. 이 어노테이션을 사용하는 클래스는 보통 하나 이상의 @Bean 어노테이션이 붙은 메소드를 포함하고, 이 메소드들은 애플리케이션 컨텍스트에서 관리될 다양한 객체(빈)를 생성하고 구성합니다.

  • 역할: Spring IoC(Inversion of Control) 컨테이너에게 이 클래스가 애플리케이션의 설정 정보를 제공하는 데 사용될 것임을 알립니다.
  • 기능: 이 클래스 내의 @Bean 어노테이션이 붙은 모든 메소드가 반환하는 객체를 Spring 컨테이너가 관리하는 빈으로 등록합니다.

3.2 @Bean

@Bean 어노테이션은 메소드 레벨에서 사용되며, 해당 메소드가 Spring 컨테이너에 의해 관리될 빈을 생성한다는 것을 나타냅니다. @Bean 어노테이션이 붙은 메소드는 보통 @Configuration 어노테이션이 붙은 클래스 내부에 위치합니다.

  • 역할: 메소드가 생성하는 객체를 Spring의 애플리케이션 컨텍스트에 빈으로 등록하도록 지시합니다.
  • 기능: 메소드가 반환하는 객체는 Spring 컨테이너에 의해 인스턴스화되고 관리됩니다. 이 객체들은 의존성 주입을 통해 애플리케이션 전체에서 사용할 수 있습니다.

3.3 사용된 코드의 맥락

이 코드에서 ModelMapper 객체를 생성하고 구성하는 메소드 getMapper에 @Bean 어노테이션이 사용되었습니다. ModelMapper는 객체 간의 매핑을 용이하게 하는 라이브러리로, 이 구성을 통해 개발자는 엔티티와 DTO(Data Transfer Object) 간의 변환 작업을 쉽게 할 수 있습니다. 설정 옵션들은 다음과 같습니다:

  • setFieldMatchingEnabled(true): 필드 매칭을 활성화하여 모델 간의 매핑시 필드 이름이 일치할 경우 자동으로 값이 할당되도록 합니다.
  • setFieldAccessLevel(Configuration.AccessLevel.PRIVATE): private 접근 수준의 필드에도 매핑할 수 있게 합니다.
  • setMatchingStrategy(MatchingStrategies.LOOSE): 매칭 전략을 LOOSE로 설정하여 좀 더 유연한 매핑이 가능하도록 합니다. (이름이 정확히 일치하지 않아도 유사한 이름을 가진 필드간에 매핑을 시도합니다.)

 

4. BoardService 예제코드

public interface BoardService {
    Long register(BoardDTO boardDTO);
}
  • BoardService 인터페이스
@Service
@RequiredArgsConstructor
@Transactional
public class BoardServiceImpl implements BoardService{
    private final ModelMapper modelMapper;
    private final BoardRepository boardRepository;

    @Override
    public Long register(BoardDTO boardDTO){
        Board board = modelMapper.map(boardDTO, Board.class);
        Long bno = boardRepository.save(board).getBno();
        return bno;
    }
}
  • BoardService 인터페이스를 구현하는 BoardServiceImpl 클래스
  • BoardDTO 객체 타입을 매개 변수로 받아 Board로 매핑한 후, 그 객체를 BoardRepository를 사용해 Board Entity에 JPA로 데이터 베이스에 접근해 CRUD를 수행한다.
  1. @Service
    • @Service 어노테이션은 해당 클래스가 서비스 계층의 컴포넌트임을 나타내는데 사용됩니다. 서비스 계층은 비즈니스 로직을 처리하며, 일반적으로 데이터 액세스 계층(Repository)과 컨트롤러 계층 사이에서 중재자 역할을 합니다. Spring 컨테이너는 @Service로 어노테이트된 클래스를 자동으로 검색하고, 빈(bean)으로 등록하여 의존성 주입을 용이하게 합니다.
  2. @RequiredArgsConstructor
    • @RequiredArgsConstructor 어노테이션은 Lombok 라이브러리에 포함되어 있으며, 모든 final 필드 또는 @NonNull 어노테이션이 붙은 필드에 대한 생성자를 자동으로 생성합니다. 이를 통해 보일러플레이트 코드를 줄이고, 객체의 불변성을 보장하는 데 도움이 됩니다. 생성자를 통해 의존성 주입이 이루어질 때 유용하게 사용됩니다.
  3. @Transactional
    • @Transactional 어노테이션은 해당 메소드 또는 클래스의 모든 public 메소드에 트랜잭션 경계를 정의합니다. 이 어노테이션은 메소드가 실행될 때 트랜잭션을 시작하고, 메소드가 성공적으로 완료되면 트랜잭션을 커밋하며, 예외가 발생하면 롤백합니다. 이는 데이터의 일관성과 무결성을 유지하는 데 중요합니다. Spring의 선언적 트랜잭션 관리 기능을 활용하여 개발자는 복잡한 트랜잭션 관리 코드를 직접 작성할 필요 없이 트랜잭션을 관리할 수 있습니다.
    1. 불변성(Immutability) 강화: final 키워드를 사용함으로써, 해당 필드는 생성 후 변경할 수 없습니다. 이는 객체가 생성된 후에 필드 값이 변경되는 것을 방지하여, 데이터의 안정성과 예측 가능성을 높입니다. 객체가 생성될 때 필요한 의존성이 제대로 설정되었는지 확실히 할 수 있으며, 일단 설정되면 변경되지 않기 때문에 다양한 부작용에서 자유롭게 됩니다.
    2. 의존성 주입(Dependency Injection) 활용: Spring Framework에서는 주로 생성자 주입 방식을 권장하며, final 필드 사용은 이와 잘 어울립니다. @RequiredArgsConstructor 어노테이션은 Lombok 라이브러리가 제공하며, 모든 final 필드에 대한 생성자를 자동으로 생성합니다. 이렇게 생성된 생성자를 통해 Spring이 필요한 의존성들을 객체 생성 시점에 주입하며, 이는 필드가 final이므로 변경될 수 없다는 점에서 안정적입니다.
    3. 스레드 안전성(Thread Safety): final로 선언된 필드는 멀티스레드 환경에서도 한 번 값이 할당되면 변경되지 않기 때문에 추가적인 동기화 없이도 스레드 안전을 보장받을 수 있습니다. 이는 특히 웹 애플리케이션과 같이 다수의 요청을 동시에 처리해야 하는 환경에서 중요합니다.
    이러한 이유들로 인해 final 키워드는 객체의 안전성, 불변성, 그리고 스레드 안전성을 보장하는 데 도움을 주어, 코드의 견고함을 향상시키고, 오류 발생 가능성을 줄이는 데 기여합니다.

5. 더 알아가기

 

5.1 BooleanBuilder의 기본적인 사용법과 특징

  • BooleanBuilder는 조건문들을 논리적으로 결합하는데 사용됩니다. 이를 통해 복잡한 쿼리 조건을 프로그래밍 방식으로 구성하고 조건들을 체계적으로 관리할 수 있습니다. 간단히 말해서, BooleanBuilder는 여러 조건을 AND, OR 등으로 연결하여 전체 쿼리의 WHERE 절을 동적으로 구성하는 데 유용합니다.

5.1.1 주요 기능

  • 초기화: BooleanBuilder는 BooleanExpression 객체로 초기화할 수 있으며, 초기 조건 없이 빈 상태로 시작할 수도 있습니다.
BooleanBuilder builder = new BooleanBuilder();
BooleanBuilder builderWithInitialCondition = new BooleanBuilder(someInitialCondition);
  • 조건 추가: BooleanBuilder에는 and, or 메소드가 있어서 추가적인 조건을 쉽게 포함시킬 수 있습니다.
builder.and(anotherCondition);
builder.or(anotherCondition);
  • 조건 확인: BooleanBuilder는 조건을 추가한 후, 언제든지 현재까지 구성된 조건을 확인할 수 있습니다.
BooleanExpression combinedExpression = builder.getValue();

5.2.2 사용 예시

  • 예를 들어, 사용자로부터 여러 필터링 조건을 입력받아 데이터베이스에서 사용자 데이터를 검색하는 경우, BooleanBuilder를 사용하면 각 조건을 검사하고 해당하는 조건만 쿼리에 추가할 수 있습니다.
BooleanBuilder builder = new BooleanBuilder();
if (username != null) {
    builder.and(user.username.eq(username));
}
if (age != null) {
    builder.and(user.age.gt(age));
}
if (isActive != null) {
    builder.and(user.isActive.eq(isActive));
}
List<User> users = queryFactory.selectFrom(user).where(builder).fetch();
  • 위 코드에서는 사용자 이름, 나이, 활성 상태 등 다양한 조건을 BooleanBuilder를 사용하여 동적으로 구성하고, 최종적으로 이 조건들을 기반으로 사용자 데이터를 검색합니다. 이렇게 BooleanBuilder를 사용하면 복잡한 조건 로직도 쉽게 관리할 수 있으며, 코드의 유지보수성도 향상됩니다.

댓글