ballqs 님의 블로그
[Spring] @Query를 사용하다 생긴 문제 , Enum 활용 , Java 설정 파일 본문
뉴스피드 게시물 조회하는 기능을 구현하고 있는 도중 발생한 문제이다.
해당 데이터들을 어떻게 가져와야 할지? 고민이 많았고 모든 과정을 기록하진 못하지만 이렇게 해서 해결했더라 정도를 기록하는 글이다.
도입부
뉴스피드 게시물 조회란?
내 게시물이랑 친구 게시물을 페이지네이션 및 정렬기준과 기한 검색을 기반으로 하는 기능이다.
해당 기능을 구현하기 위해 구성한 Entity의 구성을 작성해야 추후 내가 다시 봤을때 왜 그랬는지 이해가 쉬울 것 같다.
작성한 구조는 아래와 같다.
해당 Entity의 구성에서 뉴스피드 게시물 조회를 진행하기로 했다.
게시물 위주의 조회다보니 주가 되었던건 게시물(Board) 기반으로 코드를 작성하여 아래와 같이 틀을 잡았다.
NewsfeedController.java 작성
@RestController
@RequiredArgsConstructor
@RequestMapping("/newsfeed")
public class NewsfeedController {
private final NewsfeedService newsfeedService;
@GetMapping
public ResponseEntity<ResponseDto<Page<NewsfeedResponseDto>>> getNewsfeed(@Auth AuthUser authUser,
@ModelAttribute NewsfeedRequestDto newsfeedRequestDto) {
return ResponseEntity.ok(new ResponseDto<>(200 , newsfeedService.getNewsfeed(authUser.getUserId() , newsfeedRequestDto) , "뉴스피드 게시물 조회했습니다."));
}
}
@ModelAttribute란?
@RequestParam으로 하나하나 받아 올수 있는 것을 Dto형식으로 받아올수 있게 해주는 Annotation이다.
개별적으로 값을 받기 위해서는 Setter를 사용하여 넣어준다.
ㄴ NoArgsConstructor를 이용하고 Setter로 넣어주는 형식!!
@Getter
@Setter
public class NewsfeedRequestDto {
private int page = 1; // default 값 설정하는 방법
private NewsfeedSortEnum sort;
private String startDt;
private String endDt;
}
NewsfeedService.java 작성
@RequiredArgsConstructor
@Slf4j
@Service
public class NewsfeedService {
private final NewsfeedRepository newsfeedRepository;
private final UserRepository userRepository;
public Page<NewsfeedResponseDto> getNewsfeed(Long userId , NewsfeedRequestDto newsfeedRequestDto) {
return null;
}
}
Repository.java 작성
public interface NewsfeedRepository extends JpaRepository<Friend, Long> {
}
본론
조회하는 방법을 JPA로는 해결이 힘들어 보여서 다른 분들이 @Query를 사용해보는 것도 어떻겠냐는 의견에 한번 알아보고 진행해보았다.
NewsfeedRepository.java @Query 작성
@Query("""
select b.title AS title , b.content AS content , u.name as userName ,
ifnull(count(c.commentId),0) as commentCnt, ifnull(count(bl.boardLikeId),0) as likeCnt,
b.createdAt AS createdAt , b.modifiedAt AS modifiedAt
from Board b
inner join User u on u.userId = b.user.userId
left join Comment c on c.board.boardId = b.boardId
left join BoardLike bl on bl.board.boardId = b.boardId
where date_format(b.createdAt , '%Y-%m-%d') between :startDt and :endDt
and (b.user.userId :userId1
or b.user.userId in (
select f.friendId
from Friend f
where f.friendId.userId = :userId2
)
)
group by b.boardId
""")
위와 같이 작성하다가 계속 에러가 발생했다.
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'newsfeedController' defined in file [D:\bootcamp\project\Newsfeed\build\classes\java\main\org\sparta\newsfeed\newsfeed\controller\NewsfeedController.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'newsfeedService' defined in file [D:\bootcamp\project\Newsfeed\build\classes\java\main\org\sparta\newsfeed\newsfeed\service\NewsfeedService.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'newsfeedRepository' defined in org.sparta.newsfeed.newsfeed.repository.NewsfeedRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Could not create query for public abstract org.springframework.data.domain.Page org.sparta.newsfeed.newsfeed.repository.NewsfeedRepository.findByNewsfeed(java.lang.Long,java.lang.Long,java.lang.String,java.lang.String,org.springframework.data.domain.Pageable); Reason: Validation failed for query for method public abstract org.springframework.data.domain.Page org.sparta.newsfeed.newsfeed.repository.NewsfeedRepository.findByNewsfeed(java.lang.Long,java.lang.Long,java.lang.String,java.lang.String,org.springframework.data.domain.Pageable)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:795) ~[spring-beans-6.1.12.jar:6.1.12]
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:237) ~[spring-beans-6.1.12.jar:6.1.12]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1375) ~[spring-beans-6.1.12.jar:6.1.12]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1212) ~[spring-beans-6.1.12.jar:6.1.12]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[spring-beans-6.1.12.jar:6.1.12]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.12.jar:6.1.12]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.12.jar:6.1.12]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.12.jar:6.1.12]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.12.jar:6.1.12]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.12.jar:6.1.12]
그래서 원인을 몰라서 하나하나씩 빼보다가 subquery를 빼보기로 했고 빼니까 에러가 사라졌다??....
이유를 모른채 계속 진행해보다가 그냥 userId를 List로 넣어도 되나? 싶어서 아래와 같이 수정 작업을 진행했다.
NewsfeedService.java에 코드 수정
private final NewsfeedRepository newsfeedRepository;
private final UserRepository userRepository;
public Page<NewsfeedResponseDto> getNewsfeed(Long userId , NewsfeedRequestDto newsfeedRequestDto) {
User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("User not found"));
List<Friend> users = newsfeedRepository.findByBaseIdAndApplyYnTrue(user);
List<Long> userIds = new ArrayList<>();
userIds.add(userId);
for (Friend friend : users) {
userIds.add(friend.getFriendId().getUserId());
}
PageRequest pageRequest = PageRequest.of(newsfeedRequestDto.getPage() - 1 , 10 , Sort.by(newsfeedRequestDto.getSort().getSort()).descending());
return newsfeedRepository.findByNewsfeed(userIds , newsfeedRequestDto.getStartDt() , newsfeedRequestDto.getEndDt() , pageRequest);
}
Custom으로 만든 메서드에 Pageable 적용
Page<NewsfeedResponseDto> findByNewsfeed(
List<Long> userIds, String startDt, String endDt, Pageable pageable);
만드는 메서드에 Page<> 라는 형식으로 사용하게 되면 Pageable를 넣으라는 형식으로 사용할수 있게 된다.
PageRequest pageRequest =
PageRequest.of(newsfeedRequestDto.getPage() - 1 , 10 ,
Sort.by(newsfeedRequestDto.getSort().getSort()).descending());
그래서 PageRequest를 만들어서 메서드에 추가하여 진행했다.
NewsfeedRepository.java에 findByNewsfeed 수정
@Query("""
select b.title , b.content , u.name as userName ,
ifnull(count(c.commentId),0) as commentCnt, ifnull(count(bl.boardLikeId),0) as likeCnt,
b.createdAt , b.modifiedAt
from Board b
inner join User u on u.userId = b.user.userId
left join Comment c on c.board.boardId = b.boardId
left join BoardLike bl on bl.board.boardId = b.boardId
where date_format(b.createdAt , '%Y-%m-%d') between :startDt and :endDt
and b.user.userId in :userIds
group by b.boardId
""")
Page<NewsfeedResponseDto> findByNewsfeed(List<Long> userIds, String startDt, String endDt, Pageable pageable);
NewsfeedResponseDto 추가
public class NewsfeedResponseDto {
private String title;
private String content;
private String userName;
private Long commentCnt;
private Long likeCnt;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
}
재현을 해볼려고 다시 시도해보았지만 실패했다.
그냥 어떻게 결과값이 나왔냐면... 매칭이 안되서 null값만 나오거나
데이터가 나오지 않는 상황이 생겼다.
데이터가 제대로 나온 경우
그래서 이부분을 해결하기 위해 Dto를 고치기로 했다.
NewsfeedResponseDto를 class에서 interface로...
public interface NewsfeedResponseDto {
String getTitle();
String getContent();
String getUserName();
Long getCommentCnt();
Long getLikeCnt();
LocalDateTime getCreatedAt();
LocalDateTime getModifiedAt();
}
@Query에는 AS 추가
@Query("""
select b.title AS title , b.content AS content , u.name as userName ,
ifnull(count(c.commentId),0) as commentCnt, ifnull(count(bl.boardLikeId),0) as likeCnt,
b.createdAt AS createdAt , b.modifiedAt AS modifiedAt
from Board b
inner join User u on u.userId = b.user.userId
left join Comment c on c.board.boardId = b.boardId
left join BoardLike bl on bl.board.boardId = b.boardId
where date_format(b.createdAt , '%Y-%m-%d') between :startDt and :endDt
and b.user.userId in :userIds
group by b.boardId
""")
Page<NewsfeedResponseDto> findByNewsfeed(List<Long> userIds, String startDt, String endDt, Pageable pageable);
그리고 또 다른 2가지를 공부하게 되었다.
Enum 활용
enum 작성
@Getter // enum에 getter도 사용 가능!!
public enum NewsfeedSortEnum {
CREATEDAT("createdAt"),
MODIFIEDAT("modifiedAt"),
LIKECNT("likeCnt");
private final String sort;
NewsfeedSortEnum(String sort) {
this.sort = sort;
}
}
활용 사례
@Getter
@Setter
public class NewsfeedRequestDto {
private int page = 1;
private NewsfeedSortEnum sort;
private String startDt;
private String endDt;
}
Request에 이런식으로 받는 것도 가능해서 실제로 사용시에는 아래와 같이 사용한다.
newsfeedRequestDto.getSort().getSort();
설정 파일 따로 빼기
application.properties 설정 파일에 추가
# Read and apply everything in another configuration file
spring.profiles.include=private
# Configuration file that specifies the local and dev environment
#spring.profiles.active=private
다른 설정 파일을 include해서 가져온다.
application-private.properties 설정 파일 생성(해당 파일은 gitignore에 추가하자!)
server.port=9090
spring.jpa.hibernate.ddl-auto=validate
spring.datasource.url=jdbc:mysql://localhost:3306/newsfeed
spring.datasource.username=user
spring.datasource.password=user
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
jwt.secret.access.key=key
jwt.secret.refresh.key=key
다만 이것을 알기 전까지는 .env 형식으로 사용했으나 java 개발에는 주로 사용하지 않는 방법이라 한다.
.env 사용 방법
dependencies 추가
implementation 'io.github.cdimascio:dotenv-java:2.2.0'
application.properties 설정
spring.application.name=nbcamp-spring-jpa-task
server.port=9090
spring.datasource.url=jdbc:mysql://${DB_IP}:${DB_PORT}/${DB_NM}
spring.datasource.username=${DB_ID}
spring.datasource.password=${DB_PW}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQLDialect
jwt.secret.key=${JWT_KEY}
.env 생성
DB_IP="데이터베이스 주소"
DB_PORT="데이터베이스 포트"
DB_NM="데이터베이스"
DB_ID="아이디"
DB_PW="비밀번호"
JWT_KEY="JWT 키"
적용 방법
@EnableFeignClients
@EnableCaching
@EnableJpaAuditing
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// .env 파일 로드
// 다른 위치에 있다면 Dotenv.configure().directory("/your/path").load()를 사용하여 경로를 지정
Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
// 환경 변수를 시스템 속성으로 설정
System.setProperty("DB_IP", dotenv.get("DB_IP"));
System.setProperty("DB_PORT", dotenv.get("DB_PORT"));
System.setProperty("DB_NM", dotenv.get("DB_NM"));
System.setProperty("DB_ID", dotenv.get("DB_ID"));
System.setProperty("DB_PW", dotenv.get("DB_PW"));
System.setProperty("JWT_KEY", dotenv.get("JWT_KEY"));
SpringApplication.run(Application.class, args);
}
}
결론
막상 문제가 들이닥쳤을때 기록을 못하고 있는 상황에 아쉽다....
어느정도 해결되었을때 크게 기억에 남는 부분만 어떻게 해결할지 찾아보다가 검색보다는 수정 조금 해보면서 해결했다보니 이게 맞는지 싶기도 하다...
좀 더 정리 잘해서 미래의 나에게 도움 되기를
'코딩 공부 > Spring' 카테고리의 다른 글
[Spring] Redis 적용 (Window 환경) (0) | 2024.09.08 |
---|---|
[Spring] CustomException , ErrorCode 작성 및 적용 (0) | 2024.09.05 |
[Spring] Spring Security 기초 사용법 (0) | 2024.08.30 |
[Spring] AOP (0) | 2024.08.29 |
[Spring] Test 코드 작성(stub , mock) (0) | 2024.08.28 |