-
Notifications
You must be signed in to change notification settings - Fork 0
컨벤션
deepredk edited this page Nov 19, 2023
·
4 revisions
구글 코딩컨벤션을 지키지 않으면 노란 밑줄이 생기도록 하고, Reformat(컨트롤+알트+L) 할 때 이 설정으로 작동함
- Preferences -> Editor -> Code Style -> Java에 들어간다.
- Scheme 우측에 있는 톱니바퀴 -> Import Scheme -> Checkstyle configuration
- 프로젝트 최상위 폴더에 있는 custom_google_checks.xml을 연다
- Preferences -> Plugins -> Marketplace에 들어간다.
- CheckStyle-IDEA를 찾아 설치한다.
- Preferences -> Tools -> Checkstyle에 들어간다.
- Configuration File 밑의 +를 눌러 custom_google_checks.xml을 추가한다.
- 추가한 설정을 아래 이미지처럼 체크한 뒤 OK를 누른다.

class Class {
int field;
void method() {
}
}
- 모든 클래스의 맨 윗줄은 줄바꿈, 맨 아랫줄은 줄바꿈하지 않습니다
@NoArgsConstruct(AccessLevel.PROTECTED)
class Entity {
Long id;
int field1;
int field2;
int field3;
@Builder
public Entity(int field1, int field2, int field3) {
}
}
- 필드가 3개 이상인 클래스는 항상
@Builder
를 사용하여 객체를 생성합니다. (생성자 X) - 생성자를 만들어줘야 할 필요가 있을 때는 protected로 생성합니다. (생성자를 실수로 사용할 일이 없도록)
모든 컨트롤러에서 공통적으로 반환해야 하는 필드를 부모 클래스에 모아 미리 정의해두었음
@Getter
@RequiredArgsConstructor
public class ResponseBody<T> {
private final Status status;
private final String errorMessage;
private final T data;
public static ResponseBody<Void> ok() {
return new ResponseBody<>(Status.SUCCESS, null, null);
}
public static <T> ResponseBody<T> ok(T data) {
return new ResponseBody<>(Status.SUCCESS, null, data);
}
public static ResponseBody<Void> fail(String errorMessage) {
return new ResponseBody<>(Status.FAIL, errorMessage, null);
}
public static <T> ResponseBody<T> fail(String errorMessage, T data) {
return new ResponseBody<>(Status.FAIL, errorMessage, data);
}
public static ResponseBody<Void> error(String errorMessage) {
return new ResponseBody<>(Status.ERROR, errorMessage, null);
}
public static <T> ResponseBody<T> error(String errorMessage, T data) {
return new ResponseBody<>(Status.ERROR, errorMessage, data);
}
}
@DeleteMapping("/{id}")
public ResponseBody<Void> deleteComment(@PathVariable Long id) {
commentService.deleteComment(id);
return ResponseBody.ok();
}
@PutMapping("/{id}")
public ResponseBody<CommentEditResponse> editComment(
@Valid @RequestBody CommentEditRequest request
) {
var response = commentService.editComment(mapper.of(request));
return ResponseBody.ok(mapper.of(response));
}
@Getter
public abstract class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message) {
super(message);
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
}
- 처리할 모든 예외를 이 BaseException을 상속하여 만들면 됨
public class TokenExpireException extends BaseException {
public TokenExpireException() {
super(ErrorCode.TOKEN_EXPIRED.getErrorMsg());
}
}
// ErrorCode 클래스
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
TOKEN_EXPIRED("만료된 토큰입니다.");
private final String errorMsg;
public String getErrorMsg(Object... arg) {
return String.format(errorMsg, arg);
}
}
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = BaseException.class)
public ResponseBody<Void> handleBaseException(BaseException e) {
log.warn("[BaseException] Message = {}", e.getMessage());
return ResponseBody.fail(e.getMessage());
}
}

@Builder
public record CommentSaveRequest(
@NotBlank(message = "빈 문자열은 입력할 수 없습니다.")
String content
) {
}
-
jakarta.validation.constraints
패키지의 어노테이션을 활용 - message 필수 입력
@Mapper(
componentModel = "spring",
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
unmappedTargetPolicy = ReportingPolicy.ERROR
)
public interface TripDtoMapper {
// Request
TripSaveDto of(TripSaveRequest request);
TripUpdateDto of(TripUpdateRequest request);
// Response
TripResponse of(TripItineraryInfoDto dto);
TripSummaryResponse of(TripInfoDto dto);
TripDetailResponse of(TripDetailDto dto);
List<TripSummaryResponse> of(List<TripInfoDto> dto);
default Page<TripSummaryResponse> of(Page<TripInfoDto> dto) {
return dto.map(this::of);
}
}
- MapStruct 라이브러리를 사용한 Mapper 인터페이스를 통해 변환합니다.
- 인터페이스로 변환 내용(매개변수, 반환형)을 선언하면 자동으로 변환 코드가 짜여집니다.
@Builder
public record CommentUpdateDto(
String content
) {
public Comment toEntity() {
return Comment.builder()
.content(content)
.build();
}
}
@Builder
public record CommentInfoDto(
Long id,
TripInfoDto trip,
MemberInfoDto member,
String content
) {
public static CommentInfoDto from(Comment comment) {
return CommentInfoDto.builder()
.id(comment.getId())
.member(MemberInfoDto.from(comment.getMember()))
.trip(TripInfoDto.from(comment.getTrip()))
.content(comment.getContent())
.build();
}
}
- Service DTO의
from
,toEntity
메서드를 통해 변환합니다. -
from
: Entity -> Service DTO -
to
: Service DTO -> Entity
-
Repository:
@Query
어노테이션 등을 사용한 메서드에 대해 테스트 코드 작성 필수 - Service: 단순 Repository 메서드 호출 이상의 로직을 수행하는 메서드에 대해 테스트 코드 작성 필수
- Controller: 모든 public 메서드 테스트 코드 작성 필수
@DataJpaTest
public class CommentRepositoryTests {
@Autowired
private CommentRepository commentRepository;
@Test
void findAll() {
// given
Comment comment = new Comment("example");
Comment savedComment = commentRepository.save(comment);
// when
List<Comment> comments = commentRepository.findAll();
// then
assertThat(comments.size()).isEqualTo(1);
assertThat(comments.get(0)).isEqualTo(savedComment);
}
@Test
void findById() {
// given
Comment comment = new Comment("example");
Comment savedComment = commentRepository.save(comment);
// when
Comment foundComment = commentRepository.findById(savedComment.getId()).orElseThrow();
// then
assertThat(foundComment).isEqualTo(savedComment);
}
@Test
void deleteById() {
// given
Comment comment = new Comment("example");
Comment savedComment = commentRepository.save(comment);
// when
commentRepository.deleteById(savedComment.getId());
// then
assertThat(commentRepository.count()).isEqualTo(0);
}
@Test
void save() {
// given
Comment comment = new Comment("example");
// when
commentRepository.save(comment);
// then
List<Comment> comments = commentRepository.findAll();
assertThat(comments.size()).isEqualTo(1);
assertThat(comments.get(0).getComment()).isEqualTo("example");
}
}
@ExtendWith(MockitoExtension.class)
public class CommentServiceTests {
@InjectMocks
private CommentService commentService;
@Mock
private CommentRepository commentRepository;
@Test
void editComment_정상_동작() {
// given
Long commentId = 1L;
Comment comment = new Comment("example");
given(commentRepository.findById(commentId)) // comemntId에 대해 findById가 호출되면
.willReturn(Optional.of(comment)); // Optional.of(comment)를 반환하도록
given(commentRepository.save(any())) // 어떤 매개변수든 save가 호출되면
.willAnswer(invocation -> invocation.getArguments()[0]); // 첫번째 매개변수를 그대로 반환하도록
// when
Comment editedComment = commentService.editComment(1L, "edited");
// then
assertThat(editedComment.getComment()).isEqualTo("edited");
}
@Test
void editComment_없는_댓글을_수정하려하면_예외() {
// given
given(commentRepository.findById(any()))
.willReturn(Optional.empty());
// when, then
assertThatThrownBy(() -> {
commentService.editComment(0L, "edited");
}).isInstanceOf(NoSuchCommentException.class);
}
}
// build.gradle에 아래 디펜던시 추가 필수
testImplementation 'io.rest-assured:rest-assured:5.1.1'
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class CommentControllerTests {
@LocalServerPort
private int port;
@Autowired
private CommentRepository commentRepository;
@BeforeEach
void setUp() {
RestAssured.port = port;
}
@Test
void 댓글_목록() {
// given
String url = "/api/comments";
Comment givenComment = commentRepository.save(new Comment("example1"));
// when
ExtractableResponse<Response> response = RestAssured
.given().log().all()
.when()
.get(url)
.then().log().all()
.extract();
// then
JsonPath jsonPath = response.jsonPath();
String status = jsonPath.getString("status");
List<CommentResponse> data = jsonPath.getList("data", CommentResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
assertSoftly((softly) -> {
softly.assertThat(status).isEqualTo("SUCCESS");
softly.assertThat(data).hasSize(1);
softly.assertThat(data).contains(toCommentResponse(givenComment));
});
}
@Test
void 댓글_작성() {
// given
String url = "/api/comments";
CommentRequest request = new CommentRequest("example");
// when
ExtractableResponse<Response> response = RestAssured
.given().log().all()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(request)
.when()
.post(url)
.then().log().all()
.extract();
// then
JsonPath jsonPath = response.jsonPath();
String status = jsonPath.getString("status");
CommentResponse data = jsonPath.getObject("data", CommentResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
assertSoftly((softly) -> {
softly.assertThat(status).isEqualTo("SUCCESS");
softly.assertThat(data.id()).isNotNull();
softly.assertThat(data.comment()).isEqualTo("example");
});
// assertThat은 실패 시 다음 줄로 내려가지 않고 바로 종료되지만, assertSoftly는 실패해도 모든 줄을 실행한다
}
@Test
void 댓글_수정() {
// given
Comment givenComment = commentRepository.save(new Comment("before edit"));
String url = "/api/comments/" + givenComment.getId();
CommentRequest request = new CommentRequest("after edit");
// when
ExtractableResponse<Response> response = RestAssured
.given().log().all()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(request)
.when()
.put(url)
.then().log().all()
.extract();
// then
JsonPath jsonPath = response.jsonPath();
String status = jsonPath.getString("status");
CommentResponse data = jsonPath.getObject("data", CommentResponse.class);
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
assertSoftly((softly) -> {
softly.assertThat(status).isEqualTo("SUCCESS");
softly.assertThat(data.id()).isEqualTo(givenComment.getId());
softly.assertThat(data.comment()).isEqualTo("after edit");
});
}
@Test
void 댓글_삭제() {
// given
Comment givenComment = commentRepository.save(new Comment("before edit"));
String url = "/api/comments/" + givenComment.getId();
// when
ExtractableResponse<Response> response = RestAssured
.given().log().all()
.when()
.delete(url)
.then().log().all()
.extract();
// then
JsonPath jsonPath = response.jsonPath();
String status = jsonPath.getString("status");
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
assertThat(status).isEqualTo("SUCCESS");
assertThat(commentRepository.findById(givenComment.getId())).isNotPresent();
}
}