📌 TODO
- 회원이 푼 테스트 목록 조회 : 백엔드 기능 개발
- 회원이 만든 테스트 목록 조회 : 백엔드 기능 개발
☑️ 회원이 푼 테스트 목록 조회
1️⃣ 개발 계획
테스트 목록을 조회하는 경우 정렬 기준을 최신순으로 1가지만 두었다.
최신순으로 조회하기 위한 로직이다.
- Repository → findByMemberIdOrderByResultDateDesc(Long memberId, Pageable pageable)
- memberId에 해당하는 result
- resultDate desc로 정렬
- pageable 범위의 test
- return type : List<Result>
- Service → findResultListByMemberId
- entityToDTO 메소드 추가
- findTestTitle 메소드 추가 (Result entity에 title 속성 X, 즉 title을 따로 조회할 필요 O)
- Controller → myResultList
- GET : /result/{memberId}/{page}
2️⃣ 필요한 데이터로 DTO 추가
- 매개변수
- memberId
- page
- 리턴값
- resultId
- testId
- testTitle
- resultDate
- resultDate
리턴할 데이터를 참고하여 ResultDTO를 생성했다.
package com.timekiller.zzatool.result.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Date;
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ResultDTO {
private Long resultId; // 결과 아이디
private Long testId; // 테스트 아이디
private String testTitle; // 테스트 제목
private Long memberId; // 회원 아이디
private Integer resultScore; // 결과 점수
private Date resultDate; // 제출일
}
3️⃣ Service Interface에 기능 추상화
package com.timekiller.zzatool.result.service;
import com.timekiller.zzatool.result.dto.ResultDTO;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface ResultService {
/**
* 회원이 푼 테스트 결과 목록 조회한다.
*
* @param memberId 회원 아이디
* @param pageable 페이지
* @return 테스트 결과 목록
*/
Page<ResultDTO> findResultListByMemberId(Long memberId, Pageable pageable);
}
위와 같이 interface에 내가 구현할 기능을 추상화해주었다.
4️⃣ Repository에 필요한 JPA 메소드 추가
package com.timekiller.zzatool.result.dao;
import com.timekiller.zzatool.result.entity.Result;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ResultRepository extends JpaRepository<Result, Long> {
/**
* memberId에 해당하는 결과 목록을 resultDate desc로 정렬하여 pageable만큼 조회
*
* @param memberId회원 아이디
* @param pageable페이지
* @return 결과 목록
*/
List<Result> findByMemberIdOrderByResultDateDesc(Long memberId, Pageable pageable);
}
5️⃣ Service 기능 구현
interface에 추상화한 기능을 구현하기 위해 추가적인 메소드를 private으로 2개 추가했다.
추가한 메소드는,
- testId에 해당하는 testTitle을 return하는 것
- entity를 dto로 변환해주어 return하는 것
이다.
아래는 생성한 service class 파일이다.
package com.timekiller.zzatool.result.service;
import com.timekiller.zzatool.result.dao.ResultRepository;
import com.timekiller.zzatool.result.dto.ResultDTO;
import com.timekiller.zzatool.result.entity.Result;
import com.timekiller.zzatool.test.dao.TestRepository;
import com.timekiller.zzatool.test.service.TestServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
@Service
public class ResultServiceImpl implements ResultService {
private static final Logger logger = Logger.getLogger(TestServiceImpl.class.getName());
@Autowired private ResultRepository resultRepository;
@Autowired private TestRepository testRepository;
/* SELECT : 회원이 푼 테스트 목록 조회 */
@Override
public Page<ResultDTO> findResultListByMemberId(Long memberId, Pageable pageable) {
List<Result> resultList =
resultRepository.findByMemberIdOrderByResultDateDesc(memberId, pageable);
List<ResultDTO> resultDTOList = new ArrayList<>();
for (Result result : resultList) {
String testTitle = findTestTitle(result.getTestId());
resultDTOList.add(resultEntityToDTO(result, testTitle));
}
Result exampleResult = Result.builder().memberId(memberId).build();
ExampleMatcher exampleMatcher = ExampleMatcher.matchingAll();
Example<Result> example = Example.of(exampleResult, exampleMatcher);
Long cnt = resultRepository.count(example);
return new PageImpl<>(resultDTOList, pageable, cnt);
}
/* method : 결과 entity를 dto로 변환 */
private ResultDTO resultEntityToDTO(Result result, String testTitle) {
return ResultDTO.builder()
.resultId(result.getResultId())
.testId(result.getTestId())
.testTitle(testTitle)
.resultScore(result.getResultScore())
.resultDate(result.getResultDate())
.build();
}
/* method : testId에 해당하는 testTitle 리턴 */
private String findTestTitle(Long testId) {
try {
return testRepository.findById(testId).get().getTestTitle();
} catch (Exception e) {
return "존재하지 않는 테스트입니다";
}
}
}
6️⃣ Controller 추가
마지막으로 api를 호출할 수 있도록 controller 파일에 메소드를 생성해주었다.
@GetMapping("/{memberId}/{page}")
public Page<ResultDTO> myResultList(
@PathVariable("memberId") Long memberId, @PathVariable("page") Integer page)
throws Exception {
try {
Pageable pageable = PageRequest.of(page - 1, 20);
return resultService.findResultListByMemberId(memberId, pageable);
} catch (Exception e) {
throw new Exception(e.getMessage());
}
}
7️⃣ postman 테스트
예외 상황을 postman으로 테스트하기는 어려워서 정상적인 호출만 테스트해보았다.

넣었던 샘플 테스트가 잘 나온다.
⚠️ 오류 상황 및 해결 ⚠️
postman에서 테스트를 진행하면서 오류가 발생했었다.
Servlet.service() for servlet [dispatcherServlet]
in context with path [] threw exception
[Request processing failed:
java.lang.IllegalArgumentException:
Name for argument of type [java.lang.Long] not specified,
and parameter name information not available via reflection.
Ensure that the compiler uses the '-parameters' flag.] with root cause
먼저 이번 프로젝트는 IntelliJ로 진행하는데 Setting에서 아래와 같이 설정을 했었다.

빌드를 default인 gradle로 하지 않고 IntelliJ로 하면,
@PathVariable 어노테이션을 사용할때 변수명이 정확히 무엇인지 ("블라블라") -> 이런식으로 지정해주어야 하는 번거로움이 있다.
프로젝트를 진행하는 팀원 중 gradle로 빌드하면 오류가 생기는 팀원이 있어서 (정확히는 모르지만 PC 사양에 영향을 받는 것 같다)
우리는 IntelliJ로 진행해야 했기 때문에 그냥 다같이 변수명을 써주기로 했었다.
처음에 이 부분에서 변수명을 써주지 않아 문제인가? 했는데 써준 이후에도 오류가 발생했다.
그리고 Service class에 사용한 어노테이션에 문제가 있다는 것을 알게 되었다.
우선 @RequiredArgsConstructor를 제거해주었고,
내가 사용할 repository 변수들에 @Autowired를 붙여주었다.
이렇게 하니 오류가 해결되고 모든 것이 정상적으로 돌아갔다!
그러나 정작 정확히 무슨 이유로 오류였고, 왜 해결이 됐는지는 알 수가 없었다..
그래서 이 부분은 다음에 따로 공부해보기로 하며, 잊지 않기 위해 오류/해결에 대한 내용을 노션에 작성해두었다.
다음에 꼭 찾아봐야지 .. 🤓!!
☑️ 회원이 만든 테스트 목록 조회
1️⃣ 개발 계획
테스트 목록을 조회하는 경우 정렬 기준을 2가지로 두었다.
최신순으로 조회하는 것을 default로 두고, 조회순을 기준으로 조회하는 경우를 생각했다.
먼저, 최신순으로 조회하기 위한 로직이다.
- Repository → findByMemberIdOrderByTestDateDesc(Long memberId, Pageable pageable)
- memberId에 해당하는 test
- testDate desc로 정렬
- pageable 범위의 test
- return type : List<Test>
- Service → findTestListByMemberId
- entityToDTO 메소드 추가
- orderByTestDate 메소드 추가
- Controller → myTestList
- GET : /test/{memberId}/date/{page}
두 번째로, 조회순으로 조회하기 위한 로직이다.
- Repository → findByMemberIdOrderByTestCountDesc(Long membeerId, Pageable pageable)
- memberId에 해당하는 test
- testCount desc로 정렬
- pageable 범위의 test
- return trype : List<Test>
- Service → findTestListByMemberId
- entityToDTO 메소드 추가
- orderByTestCount 메소드 추가
- Controller → myTestList
- GET : /test/{memberId}/count/{page}
2️⃣ 필요한 데이터로 DTO 추가
- 매개변수
- memberId
- page
- 리턴값
- testId
- testTitle
- testDate
- testCount
리턴할 데이터를 참고하여 MyTestDTO를 생성했다.
import lombok.*;
import java.util.Date;
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MyTestDTO {
private Long testId; // 테스트 아이디
private String testTitle; // 테스트 제목
private Date testDate; // 테스트 생성일
private Long testCount; // 테스트 조회 수
}
3️⃣ Service Interface에 기능 추상화
/**
* 회원이 만든 테스트 목록 중 페이지에 해당하는 목록을 조회한다.
*
* @param memberId 회원 아이디
* @param order 정렬 기준: date (최신순=default), count (조회순)
* @param pageable 페이지
* @return 페이지에 해당하는 테스트 목록
*/
Page<MyTestDTO> findTestListByMemberId(Long memberId, String order, Pageable pageable);
위와 같이 interface에 내가 구현할 기능을 추상화해주었다.
4️⃣ Repository에 필요한 JPA 메소드 추가
/**
* memberId에 해당하는 테스트 목록을 testDate desc로 정렬하여 pageable만큼 조회
*
* @param memberId 회원 아이디
* @param pageable 페이지
* @return 테스트 목록
*/
List<Test> findByMemberIdOrderByTestDateDesc(Long memberId, Pageable pageable);
/**
* memberId에 해당하는 테스트 목록을 testCount desc로 정렬하여 pageable만큼 조회
*
* @param memberId 회원 아이디
* @param pageable 페이지
* @return 테스트 목록
* @throws Exception sqlException 또는 정렬 기준 오류
*/
List<Test> findByMemberIdOrderByTestCountDesc(Long memberId, Pageable pageable);
5️⃣ Service 기능 구현
interface에 추상화한 기능을 구현하기 위해 추가적인 메소드를 private으로 3개 추가했다.
추가한 메소드는,
- 최신순으로 정렬된 테스트 목록을 페이징처리하여 return하는 것
- 조회순으로 정렬된 테스트 목록을 페이징처리하여 return하는 것
- entity를 dto로 변환해주어 return하는 것
이다.
/* method : 최신순으로 정렬된 테스트 목록 리턴 */
private List<Test> orderByTestDate(Long memberId, Pageable pageable) {
try {
return testRepository.findByMemberIdOrderByTestDateDesc(memberId, pageable);
} catch (Exception e) {
throw new Exception("테스트를 조회할 수 없습니다");
}
}
/* method : 조회순으로 정렬된 테스트 목록 리턴 */
private List<Test> orderByTestCount(Long memberId, Pageable pageable) throws Exception {
try {
return testRepository.findByMemberIdOrderByTestCountDesc(memberId, pageable);
} catch (Exception e) {
throw new Exception("테스트를 조회할 수 없습니다");
}
}
/* method : 테스트 entity를 dto로 변환 */
private MyTestDTO myTestEntityToDTO(Test test) {
return MyTestDTO.builder()
.testId(test.getTestId())
.testTitle(test.getTestTitle())
.testDate(test.getTestDate())
.testCount(test.getTestCount())
.build();
}
아래는 최종적으로 service 기능을 구현한 코드이다.
/* SELECT : 회원이 만든 테스트 목록 조회 */
@Override
public Page<MyTestDTO> findTestListByMemberId(Long memberId, String order, Pageable pageable)
throws Exception {
String[] orderArr = {"date", "count"};
List<Test> testList = new ArrayList<>();
if (order.equals(orderArr[0])) testList = orderByTestDate(memberId, pageable);
else if (order.equals(orderArr[1])) testList = orderByTestCount(memberId, pageable);
else throw new Exception("잘못된 접근입니다");
if (testList.size() == 0) throw new Exception("테스트가 존재하지 않습니다");
List<MyTestDTO> myTestDTOList = new ArrayList<>();
for (Test test : testList) {
myTestDTOList.add(myTestEntityToDTO(test));
}
Test exampleTest = Test.builder().memberId(memberId).build();
ExampleMatcher exampleMatcher = ExampleMatcher.matchingAll();
Example<Test> example = Example.of(exampleTest, exampleMatcher);
Long cnt = testRepository.count(example);
return new PageImpl<>(myTestDTOList, pageable, cnt);
}
6️⃣ Controller 추가
마지막으로 api를 호출할 수 있도록 controller 파일에 메소드를 생성해주었다.
@GetMapping("/test/{memberId}/{order}/{page}")
public Page<MyTestDTO> myTestList(
@PathVariable("memberId") Long memberId,
@PathVariable("order") String order,
@PathVariable("page") Integer page)
throws Exception {
try {
Pageable pageable = PageRequest.of(page - 1, 20);
return testService.findTestListByMemberId(memberId, order, pageable);
} catch (Exception e) {
throw new Exception(e.getMessage());
}
}
7️⃣ postman 테스트
예외 처리를 해주었던 부분까지 테스트를 진행하기 위해 총 3번의 테스트를 하였다.
첫 번째, 정렬 변수가 잘못된 경우 (date, count 이외의 문자열) 이다.

잘못된 접근이다 -> 예외 처리 성공
두 번째, 회원이 만든 테스트가 존재하지 않는 경우이다.

테스트가 존재하지 않습니다 -> 예외 처리 성공
마지막으로, 정상적인 경우이다.

넣었던 샘플 테스트가 잘 나온다.
⚠️ 오류 상황 및 해결 ⚠️
postman에서 정상적인 호출의 경우의 테스트를 진행하면서 오류가 발생했었다.
2024-03-31T18:56:48.783+09:00 DEBUG 44373 --- [nio-8080-exec-1]
o.s.w.s.v.ContentNegotiatingViewResolver : Selected '*/*' given [*/*]
2024-03-31T18:56:48.961+09:00 ERROR 44373 --- [nio-8080-exec-1]
org.thymeleaf.TemplateEngine :
[THYMELEAF][http-nio-8080-exec-1] Exception processing template "test/1/1":
Error resolving template [test/1/1],
template might not exist or might not be accessible by any of the configured Template Resolvers
대강 이런 내용의 오류였고, 나는 아직 타임리프를 사용하지 않았는데 왜 오류인가를 확인하니 ..
해당 컨트롤러 파일이 @RestController가 아니라 @Controller로 어노테이션이 붙어있었다.
위에서 회원이 푼 테스트 목록을 조회하는 경우, 내가 처음으로 생성한 파일이기 때문에 바로 @RestController를 붙였는데
이번 기능은 다른 팀원이 생성한 파일에 내 코드를 작성하는 것이었기 때문에 미처 확인하지 못했다. 😅
참고로 RestController = Controller + ResponseBody의 기능을 하는데,
나는 오류를 해결하기 위해 내 메소드 위에 @ResponseBody를 추가해주었다.
이후 프론트를 진행하며 html 파일을 호출하도록 기능을 수정하면서 @ResponseBody를 빼주면 될 것 같다.
또한 위에서 개발한 회원이 푼 테스트 목록 조회의 경우도 프론트를 개발하면서
@RestController 대신 @Controller를 붙여서 수정하면 될 것 같다.
어쨌든 @ResponseBody를 추가하고 오류 해결을 하여 위의 정상적인 출력을 확인할 수 있었다! Good! 🥳
📌 3월 31일
온라인 회의를 진행했다.
각자 한 개발에 대해 이야기하고 다음 미팅까지의 목표를 잡은 뒤 종료했다.
내 목표는
- 마이페이지 FE
- 푼 테스트 조회 페이지
- 만든 테스트 조회 페이지
- 테스트 조회 페이지 BE
- 테스트 상세 조회 기능
- 한줄평 조회 기능 (& 클린봇)
- 한줄평 작성 기능
- 테스트 조회 페이지 FE
이렇다. 다음 미팅이 토요일이기 때문에 약 5일간 위의 개발을 진행할 것 같다!
클린봇을 연결해야돼서 좀 복잡할 것 같지만 ,, 아자아자 ..! 😎
마치며 ...
생각보다 Spring에서 사용하는 어노테이션에 대한 지식이 부족하다는 것을 느끼게 되었다.
틈틈이 Spring 공부를 해야겠다는 다짐과 함께 따로 공부 계획을 세우기로 ..
그리고 어노테이션 설정에 있어 잘못된 방식 (서로 맞지 않는??.. 느낌) 으로 채택하며 다양한 오류를 겪어본 것 같다.
처음엔 무슨 오류지.. 하고 당황하고 두려웠으나 (🥹) 하나씩 해결하며 묘한 쾌감을 느낄 수 있었다!
앞으로도 바로 대비할 수 있도록 오류에 대한 복습을 꼭 해야겠다.
'🐾 개발' 카테고리의 다른 글
| 프로젝트 : 짜툴 (4) 마이페이지 (0) | 2024.04.10 |
|---|---|
| 프로젝트 : 데메즈 (1) 240403 ~ 240405 (0) | 2024.04.07 |
| 프로젝트 : 짜툴 (2) 240320 ~ 240325 (0) | 2024.03.31 |
| [MacOS M2] MySQL 설치하기 (feat. homebrew) (0) | 2024.03.20 |
| [MacOS M2] homebrew 설치하기 (0) | 2024.03.20 |