JSCODE 스터디는 스프링 부트 입문 클래스에서 좋은 기억이 있었기에
뭔가 2탄 느낌인 백엔드 입문 클래스도 신청하게 됐습니다 ㅎㅎ
확실히 난이도는 이전 클래스보다 올라갔고, 이번에는 새로운 도전들을 시도해 보고 작성하려 노력해 봤습니다!
1회 차의 미션은 간단한 익명 게시판
을 통해 CRUD를 만들고 심화적으로 정렬, 페이징 등 여러 조건이 주어졌습니다!
글에서 설명하지 않는 내용들 및 1회 차에서 진행된 모든 코드는 아래 PR에서 확인할 수 있습니다!
https://github.com/JSCODE-EDU/project-class-HiiWee/pull/2
😁 익명 게시판 요구사항 분석
게시판을 구현할 때 Level1 ~ Level3까지 주어졌으며 Level3의 미션을 합쳐도 괜찮겠다는 생각에 통합적으로 요구사항을 합쳤습니다!
아래와 같이 요구사항을 정리하면서 내가 무엇을 구현하고, 어떻게 구현할지 한번 이미지 트레이닝을 할 수 있어 좋았습니다. 또한 개발에 몰두하다 보면 삼천포로 빠지는 경우가 있는데 이때 내가 무엇을 하려 했더라?를 빨리 벗어나는 데에도 좋았습니다!
공통 사항
예외 처리에 대한 공통 응답 리팩토링
- 예외에 대한 Http status 제외 예외 메시지를 반환해야 한다.
- 예외에 대한 Http status 제외 공통 에러 코드를 반환해야 한다.
- 공통 에러 코드 포맷
- -1000: 예상치 못한 서버 오류
- -2000: 요청 데이터 검증(valid) 실패
- 1000: Post(게시글) 관련 예외 코드
게시글 엔티티
- 기능사항
- ID, TITLE, CONTENT를 가진다.
- 게시글의 TITLE, CONTENT는 값 객체를 통해 포장한다.
- 검증 사항
- 게시글의 제목은 200자 이내여야 한다.
- 게시글의 내용은 5000자 이내여야 한다.
게시글 작성
- 기능 사항
- 게시글은 제목, 내용, 생성 시간을 포함한다.
- 게시글이 저장될 때 id도 같이 AUTO-INCREMENT 형식으로 저장된다.
- 게시글 작성에 성공했을 때, 응답값으로 작성된 게시글에 대한 정보를 보여주어야 한다.
- 게시글이 작성됐을 때 게시글에 대한 id 값과 작성이 완료됐다는 메시지를 보여주어야 한다.
- 검증 사항
- 게시글의 제목은 비어있으면 안 된다.
게시글 전체 조회
- 기능 사항
- 게시글을 조회할 때 id, 제목, 내용의 값이 포함돼야 한다.
- 게시글을 조회할 때
생성 시간
의 값도 포함돼야 한다. - 최근에 작성된 순으로 게시글이 조회되어야 한다.
- 데이터 조회 개수는 최대 100개까지만 할 수 있어야 한다.
특정 게시글 조회 기능
- 기능 사항
- 게시글의
id
(PK, primary key)로 특정 게시글을 조회한다. - 게시글을 조회할 때
id
,제목
,내용
,생성 시간
의 값이 포함돼야 한다.
- 게시글의
- 검증 사항
- 존재하지 않는 id의 게시글은 조회할 수 없다.
특정 게시글 수정 기능
- 기능 사항
- 게시글의 id로 특정 게시글을 수정할 수 있다.
- 게시글의 제목, 내용을 수정할 수 있다.
- 게시글 수정에 성공했을 때, 응답값으로 수정된 게시글에 대한 정보를 보여주어야 한다.
- 검증 사항
- 존재하지 않는 id의 게시글은 수정할 수 없다.
- 제목, 본문이 비어있다면 수정할 수 없다.
특정 게시글 삭제 기능
- 기능 사항
- 게시글의 id값을 통해 특정 게시글을 삭제할 수 있다.
- 검증 사항
- 존재하지 않는 id의 게시글은 삭제할 수 없다.
특정 게시글 검색 기능
- 기능 사항
-
검색 키워드
로 게시글을 검색할 수 있어야 한다. -
검색 키워드
가 포함된제목
을 가진 게시글을 전부 조회한다. - 최근에 작성된 순으로 게시글이 조회되어야 한다.
- 데이터 조회 개수는 최대 100개까지만 할 수 있어야 한다
-
- 검증 사항
-
검색 키워드
는 2글자 이상이어야 한다. -
검색 키워드
는 공백이거나, null값이면 안된다.
-
➨ 미션 진행
✅ 공통 사항
예외에 대한 공통 응답
위와 같이 Custom -> HttpStatus -> Runtime 예외를 구성하고 ControllerAdvice에서는 HttpStatus 관련 예외를 한 번에 처리하며 공통적인 응답을 할 수 있었습니다!
또한 예외에 대한 별도의 에러 코드를 같이 반환하여 프론트 측에서 조금 더 다양한 기능을 만들 수 있도록 작성했습니다.
✅ 게시글 작성
게시글 도메인
게시글 도메인에서 중점으로 생각했던 것은 원시 값에 대한 포장이었습니다.
원시 값에 대한 값 객체를 사용해 내부적인 검증을 각 값 객체에 맡기고자 했습니다.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
@Embedded
private Title title;
@Embedded
private Content content;
@CreatedDate
private LocalDateTime createdAt;
protected Post() {
}
@Builder
private Post(final Long id, final String title, final String content) {
this.id = id;
this.title = Title.from(title);
this.content = Content.from(content);
}
...
}
위의 Title, Content 객체는 단순 String으로도 나타낼 수 있지만, 그럴 경우에 제목과 본문에 대한 검증 사항이 추가된다면 모두 서비스 레이어에서 진행하게 됩니다. 차라리 이런 부분을 원시 값 객체로 분리시켜 해당 객체 내부에서 수행한다면 서비스 레이어는 깔끔해집니다.
@Embeddable
public class Title {
private static final int LIMIT_LENGTH = 200;
@Column(name = "title")
private String value;
protected Title() {
}
private Title(final String value) {
this.value = value;
}
public static Title from(final String value) {
validate(value);
return new Title(value);
}
private static void validate(final String value) {
if (value.length() > LIMIT_LENGTH) {
throw new InvalidTitleException();
}
}
...
}
@Embeddable
public class Content {
private static final int LIMIT_LENGTH = 5000;
@Lob
@Column(name = "content")
private String value;
protected Content() {
}
private Content(final String value) {
this.value = value;
}
public static Content from(final String value) {
validate(value);
return new Content(value);
}
private static void validate(final String value) {
if (value.length() > LIMIT_LENGTH) {
throw new InvalidContentException();
}
}
...
}
Auditing
Level3의 요구사항으로 생성일을 저장하라는 요구사항이 있었습니다. Spring Data JPA는 Auditing 기능을 지원하는데 감시하다는
의미로 엔티티가 생성되고 수정되는 시점을 감지하여 JPA에서 자동적으로 기록해 줍니다.
해당 기능을 이용하기 위해서는 몇 가지 설정이 필요합니다. 우선 필드에 감지기능을 사용하고자 원하는 곳에 @CreatedDate
를 달아줍니다. 이를 통해 엔티티가 생성될 때 해당 필드에 자동적으로 값이 주입됩니다.
위의 애노테이션을 적용하기 위해서는 별도의 리스너 설정이 필요합니다.
우선 클래스 레벨에 @EntityListeners(AuditingEntityListener.class)
를 작성해 엔티티를 DB에 적용하기 전에 커스텀 콜백을 발생시켜 값을 주입해 주는 동작을 합니다.
또한 auditing 기능을 활성화하기 위해서는 @EnableJpaAuditing 기능이 필요합니다.
이는 main 클래스에서 같이 작성할 수도 있지만, 별도의 config 파일로 분리하여 적용했습니다.
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
사용자 요청 dto
게시글 작성은 request를 통해 받아온 사용자의 데이터를 DB에 저장하면 됩니다.
컨트롤러에서 별다른 로직이 없으므로 메인 비즈니스 로직만 살펴보면 먼저
사용자의 요청 데이터를 받아서 서비스 레이어에 전달해 줍니다.
이때 Validation을 통해 제목을 입력하지 않는 경우에는 데이터 바인딩에 실패하게 됩니다.
지금 생각해 보면 제목이 없다면, 제목 없음
과 같이 저장할 수 있도록 하는 방식도 괜찮다는 생각이 듭니다!
서비스 레이어
서비스는 dto -> entity에 작업을 수행하고, 게시글을 생성한 후 생성된 postId를 반환합니다.
각 도메인 내부에서 필요한 데이터를 스스로 검증하고 있으므로 서비스 레이어의 코드는 상당히 간결해짐을 느꼈습니다!
✅ 게시글 전체 조회
게시글 전체 조회의 경우 일반적인 조회이지만, 최대 100개까지의 게시글을 볼 수 있고, 생성시간 기준 내림차순 정렬로 조회해야 합니다.
처음에는 단순히 Spring Data JPA가 제공해 주는 쿼리를 이용해 구성할까 했습니다. 하지만, 요구사항의 변화에 유연하게 대처하기 어렵다고 생각했습니다.
Repository에 작성해 두면 편리하지만, 100개가 아닌 50개가 되고, 오름차순 정렬로 반환해야 한다면 결국 Repository 및 Service레이어에 모두 변화를 줍니다. 이는 좋지 못했습니다.
따라서 Pageable을 활용하기로 했습니다.
페이징 기능의 디폴트 값으로 생성시간 내림차순 기준과 최대 100개의 게시글을 제공하도록 하지만, 원한다면 클라이언트에서 요청을 변경하여 요구할 수 있도록 요청 파라미터
를 통해 전달받을 수 있도록 구성했습니다.
이후 서비스 레이어에서는 해당 Pageable객체를 통해 페이징을 하여 조회해 옵니다.
✅ 특정 게시글 수정
게시글에 대한 수정 자체의 비즈니스 로직은 어렵지 않았습니다. 같은 트랜잭션에 포함되므로 조회해 온 post를 update 하는 메소드를 통해 변경감지 기능을 이용하고 변경된 post를 dto로 반환합니다.
게시글에 대한 수정을 할 때 PUT, PATCH 중에서 어떤 HTTP Method를 활용해야 할지 고민이 있었습니다.
결국 서비스 코드에서 모든 항목에 대해 수정을 하고 있고 dto에서도 두 값 모두 빈값을 막아두고 있기에 PutMapping으로 결정했습니다.
✅ 특정 게시글 검색 기능
특정 게시글에 대한 검색 기능을 살펴보면
기존 전체 게시글에 대한 요구사항에서 단순히 제목으로 조회한다는 요구사항이 추가됐음을 알 수 있었습니다.
제목에 해당되는 모든 게시글을 조회해야 하므로 새로운 API를 제공하기보다는 기존 전체 게시글 조회에서 쿼리 파라미터를 통해 필터링 옵션을 주었습니다.
컨트롤러에서는 keyword를 파라미터로 받아와서 null 체크만 해줍니다. null이라면 전체 게시글을 아니라면 키워드에 해당되는 게시글을 조회합니다.
이때 키워드의 유효성을 Keyword 객체가 직접 하는데 내부적으로 공백을 제거한 키워드의 길이를 파악하여 2 미만인 경우에는 예외가 발생하고 그렇지 않다면 SQL의 LIKE
를 이용하기 위해 앞 뒤에 %
를 붙여 저장합니다.
이후 키워드 및 페이징을 통해 조회한 모든 게시글을 반환해 줍니다.
🤔 마치면서
어떻게 하면 더 확장성이 있게 작성할 수 있을까? 어떻게 하면 더 정확한 테스트를 작성할 수 있을까?
1회 차를 진행하면서 가장 많이 했던 고민이었습니다.
가장 크게 느끼고 체감한 건 100%의 fit을 가진 방법은 없다였습니다. 각각의 방식마다 서로 트레이드오프가 존재했습니다.
따라서 본인 스스로가 각각의 방식에 대해 이해하고, 적절한 상황에 적절한 방식을 사용하는 것이 정말 중요하다는 생각이 듭니다.
항상 근거 있는 코드를 작성하려고 노력해야겠습니다!
JSCODE 스터디는 스프링 부트 입문 클래스에서 좋은 기억이 있었기에
뭔가 2탄 느낌인 백엔드 입문 클래스도 신청하게 됐습니다 ㅎㅎ
확실히 난이도는 이전 클래스보다 올라갔고, 이번에는 새로운 도전들을 시도해 보고 작성하려 노력해 봤습니다!
1회 차의 미션은 간단한 익명 게시판
을 통해 CRUD를 만들고 심화적으로 정렬, 페이징 등 여러 조건이 주어졌습니다!
글에서 설명하지 않는 내용들 및 1회 차에서 진행된 모든 코드는 아래 PR에서 확인할 수 있습니다!
https://github.com/JSCODE-EDU/project-class-HiiWee/pull/2
😁 익명 게시판 요구사항 분석
게시판을 구현할 때 Level1 ~ Level3까지 주어졌으며 Level3의 미션을 합쳐도 괜찮겠다는 생각에 통합적으로 요구사항을 합쳤습니다!
아래와 같이 요구사항을 정리하면서 내가 무엇을 구현하고, 어떻게 구현할지 한번 이미지 트레이닝을 할 수 있어 좋았습니다. 또한 개발에 몰두하다 보면 삼천포로 빠지는 경우가 있는데 이때 내가 무엇을 하려 했더라?를 빨리 벗어나는 데에도 좋았습니다!
공통 사항
예외 처리에 대한 공통 응답 리팩토링
- 예외에 대한 Http status 제외 예외 메시지를 반환해야 한다.
- 예외에 대한 Http status 제외 공통 에러 코드를 반환해야 한다.
- 공통 에러 코드 포맷
- -1000: 예상치 못한 서버 오류
- -2000: 요청 데이터 검증(valid) 실패
- 1000: Post(게시글) 관련 예외 코드
게시글 엔티티
- 기능사항
- ID, TITLE, CONTENT를 가진다.
- 게시글의 TITLE, CONTENT는 값 객체를 통해 포장한다.
- 검증 사항
- 게시글의 제목은 200자 이내여야 한다.
- 게시글의 내용은 5000자 이내여야 한다.
게시글 작성
- 기능 사항
- 게시글은 제목, 내용, 생성 시간을 포함한다.
- 게시글이 저장될 때 id도 같이 AUTO-INCREMENT 형식으로 저장된다.
- 게시글 작성에 성공했을 때, 응답값으로 작성된 게시글에 대한 정보를 보여주어야 한다.
- 게시글이 작성됐을 때 게시글에 대한 id 값과 작성이 완료됐다는 메시지를 보여주어야 한다.
- 검증 사항
- 게시글의 제목은 비어있으면 안 된다.
게시글 전체 조회
- 기능 사항
- 게시글을 조회할 때 id, 제목, 내용의 값이 포함돼야 한다.
- 게시글을 조회할 때
생성 시간
의 값도 포함돼야 한다. - 최근에 작성된 순으로 게시글이 조회되어야 한다.
- 데이터 조회 개수는 최대 100개까지만 할 수 있어야 한다.
특정 게시글 조회 기능
- 기능 사항
- 게시글의
id
(PK, primary key)로 특정 게시글을 조회한다. - 게시글을 조회할 때
id
,제목
,내용
,생성 시간
의 값이 포함돼야 한다.
- 게시글의
- 검증 사항
- 존재하지 않는 id의 게시글은 조회할 수 없다.
특정 게시글 수정 기능
- 기능 사항
- 게시글의 id로 특정 게시글을 수정할 수 있다.
- 게시글의 제목, 내용을 수정할 수 있다.
- 게시글 수정에 성공했을 때, 응답값으로 수정된 게시글에 대한 정보를 보여주어야 한다.
- 검증 사항
- 존재하지 않는 id의 게시글은 수정할 수 없다.
- 제목, 본문이 비어있다면 수정할 수 없다.
특정 게시글 삭제 기능
- 기능 사항
- 게시글의 id값을 통해 특정 게시글을 삭제할 수 있다.
- 검증 사항
- 존재하지 않는 id의 게시글은 삭제할 수 없다.
특정 게시글 검색 기능
- 기능 사항
-
검색 키워드
로 게시글을 검색할 수 있어야 한다. -
검색 키워드
가 포함된제목
을 가진 게시글을 전부 조회한다. - 최근에 작성된 순으로 게시글이 조회되어야 한다.
- 데이터 조회 개수는 최대 100개까지만 할 수 있어야 한다
-
- 검증 사항
-
검색 키워드
는 2글자 이상이어야 한다. -
검색 키워드
는 공백이거나, null값이면 안된다.
-
➨ 미션 진행
✅ 공통 사항
예외에 대한 공통 응답
위와 같이 Custom -> HttpStatus -> Runtime 예외를 구성하고 ControllerAdvice에서는 HttpStatus 관련 예외를 한 번에 처리하며 공통적인 응답을 할 수 있었습니다!
또한 예외에 대한 별도의 에러 코드를 같이 반환하여 프론트 측에서 조금 더 다양한 기능을 만들 수 있도록 작성했습니다.
✅ 게시글 작성
게시글 도메인
게시글 도메인에서 중점으로 생각했던 것은 원시 값에 대한 포장이었습니다.
원시 값에 대한 값 객체를 사용해 내부적인 검증을 각 값 객체에 맡기고자 했습니다.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
@Embedded
private Title title;
@Embedded
private Content content;
@CreatedDate
private LocalDateTime createdAt;
protected Post() {
}
@Builder
private Post(final Long id, final String title, final String content) {
this.id = id;
this.title = Title.from(title);
this.content = Content.from(content);
}
...
}
위의 Title, Content 객체는 단순 String으로도 나타낼 수 있지만, 그럴 경우에 제목과 본문에 대한 검증 사항이 추가된다면 모두 서비스 레이어에서 진행하게 됩니다. 차라리 이런 부분을 원시 값 객체로 분리시켜 해당 객체 내부에서 수행한다면 서비스 레이어는 깔끔해집니다.
@Embeddable
public class Title {
private static final int LIMIT_LENGTH = 200;
@Column(name = "title")
private String value;
protected Title() {
}
private Title(final String value) {
this.value = value;
}
public static Title from(final String value) {
validate(value);
return new Title(value);
}
private static void validate(final String value) {
if (value.length() > LIMIT_LENGTH) {
throw new InvalidTitleException();
}
}
...
}
@Embeddable
public class Content {
private static final int LIMIT_LENGTH = 5000;
@Lob
@Column(name = "content")
private String value;
protected Content() {
}
private Content(final String value) {
this.value = value;
}
public static Content from(final String value) {
validate(value);
return new Content(value);
}
private static void validate(final String value) {
if (value.length() > LIMIT_LENGTH) {
throw new InvalidContentException();
}
}
...
}
Auditing
Level3의 요구사항으로 생성일을 저장하라는 요구사항이 있었습니다. Spring Data JPA는 Auditing 기능을 지원하는데 감시하다는
의미로 엔티티가 생성되고 수정되는 시점을 감지하여 JPA에서 자동적으로 기록해 줍니다.
해당 기능을 이용하기 위해서는 몇 가지 설정이 필요합니다. 우선 필드에 감지기능을 사용하고자 원하는 곳에 @CreatedDate
를 달아줍니다. 이를 통해 엔티티가 생성될 때 해당 필드에 자동적으로 값이 주입됩니다.
위의 애노테이션을 적용하기 위해서는 별도의 리스너 설정이 필요합니다.
우선 클래스 레벨에 @EntityListeners(AuditingEntityListener.class)
를 작성해 엔티티를 DB에 적용하기 전에 커스텀 콜백을 발생시켜 값을 주입해 주는 동작을 합니다.
또한 auditing 기능을 활성화하기 위해서는 @EnableJpaAuditing 기능이 필요합니다.
이는 main 클래스에서 같이 작성할 수도 있지만, 별도의 config 파일로 분리하여 적용했습니다.
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
사용자 요청 dto
게시글 작성은 request를 통해 받아온 사용자의 데이터를 DB에 저장하면 됩니다.
컨트롤러에서 별다른 로직이 없으므로 메인 비즈니스 로직만 살펴보면 먼저
사용자의 요청 데이터를 받아서 서비스 레이어에 전달해 줍니다.
이때 Validation을 통해 제목을 입력하지 않는 경우에는 데이터 바인딩에 실패하게 됩니다.
지금 생각해 보면 제목이 없다면, 제목 없음
과 같이 저장할 수 있도록 하는 방식도 괜찮다는 생각이 듭니다!
서비스 레이어
서비스는 dto -> entity에 작업을 수행하고, 게시글을 생성한 후 생성된 postId를 반환합니다.
각 도메인 내부에서 필요한 데이터를 스스로 검증하고 있으므로 서비스 레이어의 코드는 상당히 간결해짐을 느꼈습니다!
✅ 게시글 전체 조회
게시글 전체 조회의 경우 일반적인 조회이지만, 최대 100개까지의 게시글을 볼 수 있고, 생성시간 기준 내림차순 정렬로 조회해야 합니다.
처음에는 단순히 Spring Data JPA가 제공해 주는 쿼리를 이용해 구성할까 했습니다. 하지만, 요구사항의 변화에 유연하게 대처하기 어렵다고 생각했습니다.
Repository에 작성해 두면 편리하지만, 100개가 아닌 50개가 되고, 오름차순 정렬로 반환해야 한다면 결국 Repository 및 Service레이어에 모두 변화를 줍니다. 이는 좋지 못했습니다.
따라서 Pageable을 활용하기로 했습니다.
페이징 기능의 디폴트 값으로 생성시간 내림차순 기준과 최대 100개의 게시글을 제공하도록 하지만, 원한다면 클라이언트에서 요청을 변경하여 요구할 수 있도록 요청 파라미터
를 통해 전달받을 수 있도록 구성했습니다.
이후 서비스 레이어에서는 해당 Pageable객체를 통해 페이징을 하여 조회해 옵니다.
✅ 특정 게시글 수정
게시글에 대한 수정 자체의 비즈니스 로직은 어렵지 않았습니다. 같은 트랜잭션에 포함되므로 조회해 온 post를 update 하는 메소드를 통해 변경감지 기능을 이용하고 변경된 post를 dto로 반환합니다.
게시글에 대한 수정을 할 때 PUT, PATCH 중에서 어떤 HTTP Method를 활용해야 할지 고민이 있었습니다.
결국 서비스 코드에서 모든 항목에 대해 수정을 하고 있고 dto에서도 두 값 모두 빈값을 막아두고 있기에 PutMapping으로 결정했습니다.
✅ 특정 게시글 검색 기능
특정 게시글에 대한 검색 기능을 살펴보면
기존 전체 게시글에 대한 요구사항에서 단순히 제목으로 조회한다는 요구사항이 추가됐음을 알 수 있었습니다.
제목에 해당되는 모든 게시글을 조회해야 하므로 새로운 API를 제공하기보다는 기존 전체 게시글 조회에서 쿼리 파라미터를 통해 필터링 옵션을 주었습니다.
컨트롤러에서는 keyword를 파라미터로 받아와서 null 체크만 해줍니다. null이라면 전체 게시글을 아니라면 키워드에 해당되는 게시글을 조회합니다.
이때 키워드의 유효성을 Keyword 객체가 직접 하는데 내부적으로 공백을 제거한 키워드의 길이를 파악하여 2 미만인 경우에는 예외가 발생하고 그렇지 않다면 SQL의 LIKE
를 이용하기 위해 앞 뒤에 %
를 붙여 저장합니다.
이후 키워드 및 페이징을 통해 조회한 모든 게시글을 반환해 줍니다.
🤔 마치면서
어떻게 하면 더 확장성이 있게 작성할 수 있을까? 어떻게 하면 더 정확한 테스트를 작성할 수 있을까?
1회 차를 진행하면서 가장 많이 했던 고민이었습니다.
가장 크게 느끼고 체감한 건 100%의 fit을 가진 방법은 없다였습니다. 각각의 방식마다 서로 트레이드오프가 존재했습니다.
따라서 본인 스스로가 각각의 방식에 대해 이해하고, 적절한 상황에 적절한 방식을 사용하는 것이 정말 중요하다는 생각이 듭니다.
항상 근거 있는 코드를 작성하려고 노력해야겠습니다!