Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

날짜 / 시간 및 기타 코드 컨벤션 통일 #78

Merged
merged 18 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d499e8f
style: 클래스 선언부 아래 개행 추가
pricelees Jul 18, 2024
ee33702
refactor: 반환 타입 void를 ResponseEntity<Void> 타입으로 수정
pricelees Jul 18, 2024
6f9192e
style: return문 위 개행 추가
pricelees Jul 18, 2024
31b0447
style: 모든 id 변수명을 moimId로 수정
pricelees Jul 18, 2024
2a5587a
refactor: 모든 원시타입 변수 포장
pricelees Jul 18, 2024
7007393
refactor: 원시값 포장에서 발생하는 NPE 해결
pricelees Jul 18, 2024
9102746
refactor: 테스트 Assertion에서 Service가 아닌 Repository를 사용하도록 수정
pricelees Jul 18, 2024
a50b08f
refactor: 메서드 파라미터로 쓰이는 service 코드를 별도의 변수로 추출
pricelees Jul 18, 2024
898a780
refactor: 요청 및 응답에 사용되는 날짜 및 시간 형식 통일 및 테스트 추가
pricelees Jul 19, 2024
23ff318
merge: 최신 상태 반영
pricelees Jul 19, 2024
c52c39b
refactor: 반환 타입 수정 및 원시타입 변수 포장
pricelees Jul 19, 2024
10b4f6e
refactor: 반환 타입 수정
pricelees Jul 20, 2024
1d99f69
Merge branch 'develop-backend' into refactor/#73
pricelees Jul 22, 2024
237f9d6
refactor: property에 지정한 날짜, 시간 포맷 제거
pricelees Jul 22, 2024
aa54ad6
refactor: 도메인에서의 wrapper type 언박싱 및 빌더 사용 생성자 추가
pricelees Jul 22, 2024
3d6a9d6
refactor: 응답에서의 wrapper type 언박싱
pricelees Jul 22, 2024
db12372
style: 코드 컨벤션 통일
pricelees Jul 22, 2024
31a432c
refactor: 테스트에서의 Optional 처리 수정
pricelees Jul 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
@Getter
@AllArgsConstructor
public class RestResponse<T> {

private T data;
}
1 change: 1 addition & 0 deletions backend/src/main/java/mouda/backend/config/CorsConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
Expand Down
32 changes: 32 additions & 0 deletions backend/src/main/java/mouda/backend/config/JacksonConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package mouda.backend.config;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

@Configuration
public class JacksonConfig {

@Bean
public JavaTimeModule javaTimeModule() {
JavaTimeModule javaTimeModule = new JavaTimeModule();
DateTimeFormatter dateFormat = DateTimeFormatter.ISO_LOCAL_DATE;
DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm");

javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(dateFormat))
.addSerializer(LocalTime.class, new LocalTimeSerializer(timeFormat))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(dateFormat))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(timeFormat));

return javaTimeModule;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,39 @@ public class MoimController implements MoimSwagger {
@PostMapping
public ResponseEntity<RestResponse<Long>> createMoim(@RequestBody MoimCreateRequest moimCreateRequest) {
Moim moim = moimService.createMoim(moimCreateRequest);

return ResponseEntity.ok().body(new RestResponse<>(moim.getId()));
}

@Override
@GetMapping
public ResponseEntity<RestResponse<MoimFindAllResponses>> findAllMoim() {
return ResponseEntity.ok().body(new RestResponse<>(moimService.findAllMoim()));
MoimFindAllResponses moimFindAllResponses = moimService.findAllMoim();

return ResponseEntity.ok().body(new RestResponse<>(moimFindAllResponses));
}

@Override
@GetMapping("/{moimId}")
public ResponseEntity<RestResponse<MoimDetailsFindResponse>> findMoimDetails(@PathVariable long moimId) {
return ResponseEntity.ok().body(new RestResponse<>(moimService.findMoimDetails(moimId)));
public ResponseEntity<RestResponse<MoimDetailsFindResponse>> findMoimDetails(@PathVariable Long moimId) {
MoimDetailsFindResponse moimDetailsFindResponse = moimService.findMoimDetails(moimId);

return ResponseEntity.ok().body(new RestResponse<>(moimDetailsFindResponse));
}

@Override
@PostMapping("/join")
public ResponseEntity<RestResponse<Void>> joinMoim(@RequestBody MoimJoinRequest moimJoinRequest) {
public ResponseEntity<Void> joinMoim(@RequestBody MoimJoinRequest moimJoinRequest) {
moimService.joinMoim(moimJoinRequest);

return ResponseEntity.ok().build();
}

@Override
@DeleteMapping("/{moimId}")
public void deleteMoim(@PathVariable long moimId) {
public ResponseEntity<Void> deleteMoim(@PathVariable Long moimId) {
moimService.deleteMoim(moimId);

return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ public interface MoimSwagger {
@ApiResponses({
@ApiResponse(responseCode = "200", description = "모임 상세 조회 성공!"),
})
ResponseEntity<RestResponse<MoimDetailsFindResponse>> findMoimDetails(@PathVariable long moimId);
ResponseEntity<RestResponse<MoimDetailsFindResponse>> findMoimDetails(@PathVariable Long moimId);

@Operation(summary = "모임 참여", description = "모임에 참여한다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "모임 참여 성공!")
})
ResponseEntity<RestResponse<Void>> joinMoim(@RequestBody MoimJoinRequest moimJoinRequest);
ResponseEntity<Void> joinMoim(@RequestBody MoimJoinRequest moimJoinRequest);

@Operation(summary = "모임 삭제", description = "해당하는 id의 모임을 삭제한다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "모임 삭제 성공!"),
})
void deleteMoim(@PathVariable long moimId);
ResponseEntity<Void> deleteMoim(@PathVariable Long moimId);
}
24 changes: 21 additions & 3 deletions backend/src/main/java/mouda/backend/moim/domain/Moim.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -18,10 +17,9 @@

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Moim {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand All @@ -42,6 +40,26 @@ public class Moim {

private String description;

@Builder
public Moim(
String title,
LocalDate date,
LocalTime time,
String place,
int maxPeople,
String authorNickname,
String description
) {
this.title = title;
this.date = date;
this.time = time;
this.place = place;
this.currentPeople = 1;
this.maxPeople = maxPeople;
this.authorNickname = authorNickname;
this.description = description;
}

public void join() {
if (currentPeople + 1 > maxPeople) {
throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.MAX_PEOPLE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ public record MoimCreateRequest(
LocalDate date,
LocalTime time,
String place,
int maxPeople,
Integer maxPeople,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[질문] 프론트엔드 측에서 maxPeople 값을 입력해주지 않으면 이 부분이 NULL로 들어오게 되는건가요?

[의견] 추후에 Bean Validation 과 LocalTime, LocalDate 포맷팅 에러를 핸들링하는 것도 필요하겠네요 !

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[질문] 프론트엔드 측에서 maxPeople 값을 입력해주지 않으면 이 부분이 NULL로 들어오게 되는건가요?

이 부분은 어떻게 처리하는지에 따라 달라져요. 기본적으로는 Json에 maxPeople 필드가 없거나 빈 문자열이 들어오면 NULL이 입력되고, 숫자 이외의 값이 입력되면 HttpMessageNotReadableException이 발생합니다😀

[의견] 추후에 Bean Validation 과 LocalTime, LocalDate 포맷팅 에러를 핸들링하는 것도 필요하겠네요 !

조아요!👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 그렇군요 굿굿 👍
나중에 숫자 이외의 입력에 대해서도 에러 핸들링을 할 필요가 있겠네요.

String authorNickname,
String description
) {

public Moim toEntity() {
return Moim.builder()
.title(title)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public record MoimDetailsFindResponse(
String authorNickname,
String description
) {

public static MoimDetailsFindResponse toResponse(Moim moim) {
return MoimDetailsFindResponse.builder()
.title(moim.getTitle())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@Builder
public record MoimFindAllResponse(
long moimId,
Long moimId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요청 DTO에는 null 처리를 위해서 래퍼 타입을 사용하는 것은 맞지만

응답 DTO에서는 moimId가 null 인 상황이 있을까요? 앞서 엔티티에 래퍼 타입을 사용하는 것과 유사한 맥락의 의견입니다! 저는 null 일 수 없는 데이터라면 원시 타입을 사용해야한다고 생각해요.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요청 DTO에는 null 처리를 위해서 래퍼 타입을 사용하는 것은 맞지만

응답 DTO에서는 moimId가 null 인 상황이 있을까요? 앞서 엔티티에 래퍼 타입을 사용하는 것과 유사한 맥락의 의견입니다! 저는 null 일 수 없는 데이터라면 원시 타입을 사용해야한다고 생각해요.

(지극히 개인적인 관점)
원시 타입의 사용은 성능상의 이점이 있다는 이유가 크겠죠? 이 부분을 고려하지 않은 것은 아니지만 지금의 프로그램에서는 성능상의 이점보다 코드의 일관성이 더 큰 장점이라고 생각했어요. 원시 타입으로 바꾸는 것은 크게 어렵지 않지만.. 지금 단계에서 지정하는 것 보다 추후에 성능 이슈가 발생했을 때 바꿔보고 개선되는 것을 직접 확인하는 과정이 더 의미있다고 생각했습니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음음 좋은 의견이에요!
하지만 저는 성능상의 이점보다는 코드를 봤을 때 개발자가 아 이 데이터는 NULL이 들어올 수 있는 상황이구나를 래퍼 타입을 사용함으로써 오해를 불러일으킬 수 있다고 생각했어요.

성능 이슈는 아마 발생하지 않을 것 같고, 래퍼 타입 사용에 대한 일관성이

  1. 요청 DTO 2. 응답 DTO 3. 엔티티
    세 가지 중 어디까지 영향을 미치는 지에 대해 논의해봅시다 ! 👍

String title,
LocalDate date,
LocalTime time,
Expand All @@ -18,6 +18,7 @@ public record MoimFindAllResponse(
String authorNickname,
String description
) {

public static MoimFindAllResponse toResponse(Moim moim) {
return MoimFindAllResponse.builder()
.moimId(moim.getId())
Expand Down
176 changes: 176 additions & 0 deletions backend/src/test/java/mouda/backend/config/JacksonConfigTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package mouda.backend.config;

import static org.hamcrest.Matchers.*;

import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Map;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import io.restassured.RestAssured;
import mouda.backend.moim.domain.Moim;
import mouda.backend.moim.repository.MoimRepository;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class JacksonConfigTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트가 꼼꼼해서 행복하네요

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상돌 폼 미쳤다

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상돌 미쳤다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

도레미~


@Autowired
private MoimRepository moimRepository;

@Autowired
private DatabaseCleaner databaseCleaner;

@LocalServerPort
private int port;

@BeforeEach
void setUp() {
RestAssured.port = port;
}

@AfterEach
void tearDown() {
databaseCleaner.cleanUp();
}

@DisplayName("날짜 형식에 따른 직렬화 및 역직렬화 테스트")
@Nested
class DateFormatTest {

@DisplayName("yyyy-MM-dd 형식의 날짜를 역직렬화한다.")
@Test
void deserialize() {
Map<String, Object> params = Map.of(
"title", "title",
"date", "2024-07-19",
"time", "12:30",
"place", "place",
"maxPeople", 10,
"authorNickname", "안나",
"description", "설명"
);

RestAssured.given()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(params)
.when().post("/v1/moim")
.then().statusCode(is(HttpStatus.OK.value()));
}

@DisplayName("yyyy-MM-dd 형식이 아닌 날짜가 입력되면 예외가 발생한다.")
@Test
void deserialize_when_invalidDateFormat() {
Map<String, Object> params = Map.of(
"title", "title",
"date", "2024/07/19",
"time", "12:30",
"place", "place",
"maxPeople", 10,
"authorNickname", "안나",
"description", "설명"
);

RestAssured.given()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(params)
.when().post("/v1/moim")
.then().statusCode(is(HttpStatus.BAD_REQUEST.value()));
}

@DisplayName("날짜는 yyyy-MM-dd 형식으로 직렬화된다.")
@Test
void serialize() {
Moim moim = Moim.builder()
.title("title")
.date(LocalDate.parse("2024-07-19"))
.time(LocalTime.parse("12:30"))
.place("place")
.maxPeople(10)
.authorNickname("안나")
.description("설명")
.build();

Moim saved = moimRepository.save(moim);

RestAssured.given()
.when().get("/v1/moim/" + saved.getId())
.then().statusCode(is(HttpStatus.OK.value()))
.body("data.date", is("2024-07-19"));
}
}

@DisplayName("시간 형식에 따른 직렬화 및 역직렬화 테스트")
@Nested
class TimeFormatTest {

@DisplayName("HH:mm 형식의 시간을 역직렬화한다.")
@Test
void deserialize() {
Map<String, Object> params = Map.of(
"title", "title",
"date", "2024-07-19",
"time", "12:30",
"place", "place",
"maxPeople", 10,
"authorNickname", "안나",
"description", "설명"
);

RestAssured.given()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(params)
.when().post("/v1/moim")
.then().statusCode(is(HttpStatus.OK.value()));
}

@DisplayName("HH:mm 형식이 아닌 시간이 입력되면 예외가 발생한다.")
@Test
void deserialize_when_invalidTimeFormat() {
Map<String, Object> params = Map.of(
"title", "title",
"date", "2024-07-19",
"time", "12-30",
"place", "place",
"maxPeople", 10,
"authorNickname", "안나",
"description", "설명"
);

RestAssured.given()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(params)
.when().post("/v1/moim")
.then().statusCode(is(HttpStatus.BAD_REQUEST.value()));
}

@DisplayName("시간은 HH:mm 형식으로 직렬화된다.")
@Test
void serialize() {
Moim moim = Moim.builder()
.title("title")
.date(LocalDate.now().plusDays(1))
.time(LocalTime.parse("12:30"))
.place("place")
.maxPeople(10)
.authorNickname("안나")
.description("설명")
.build();
Moim saved = moimRepository.save(moim);

RestAssured.given()
.when().get("/v1/moim/" + saved.getId())
.then().statusCode(is(HttpStatus.OK.value()))
.body("data.time", is("12:30"));
}
}
}
Loading
Loading