꿈틀꿈틀 개발일기

Notice - 개인과제 (복습/개선)

by jeongminy

개인 과제 수행 겸 여태까지 배운 것들을 처음부터 다시 복습하는 느낌으로

공지사항 게시판을 개발해보는 간단한 프로젝트를 처음부터 혼자서 만들어 보려 한다.

 

https://github.com/jeongminy/notice

 

GitHub - jeongminy/notice

Contribute to jeongminy/notice development by creating an account on GitHub.

github.com

 

1. 프로젝트 생성

 

2. github 연결

 

3. 초기 설정

build.gradle.kts 에 의존성 추가

kotlin("plugin.noarg") version "1.8.22"

	implementation("org.springframework.boot:spring-boot-starter-validation") //validation
	implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0") //swager
	implementation("org.springframework.boot:spring-boot-starter-data-jpa") //jpa
	implementation("org.springframework.boot:spring-boot-starter-aop") //Spring AOP
	implementation("org.springframework.boot:spring-boot-starter-security") //Spring Security
	implementation("io.jsonwebtoken:jjwt-api:0.12.3") //JWT

	runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3") //JWT
	runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3") //JWT
	runtimeOnly("org.postgresql:postgresql") //postgresql

noArg {
	annotation("jakarta.persistence.Entity")
	annotation("jakarta.persistence.MappedSuperclass")
	annotation("jakarta.persistence.Embeddable")
}

allOpen {
	annotation("jakarta.persistence.Entity")
	annotation("jakarta.persistence.MappedSuperclass")
	annotation("jakarta.persistence.Embeddable")
}

 

API와 ERD는 기존 todolist의 설계를 참고하겠다. (달라질 수 있음)

 

 

4. Supabase 연결 및 환경변수 설정

테이블 생성

CREATE TABLE app_user (
  id BIGSERIAL PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  password TEXT NOT NULL,
  nickname TEXT NOT NULL,
  role TEXT NOT NULL
)

CREATE TABLE post (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGSERIAL NOT NULL,
  title TEXT NOT NULL,
  description TEXT NOT NULL,
  status TEXT NOT NULL,
  post_image_url TEXT NOT NULL,
  created_at timestamp with time zone,
  updated_at timestamp with time zone,
  CONSTRAINT post_user FOREIGN KEY (user_id) REFERENCES app_user
);

CREATE TABLE comment (
  id BIGSERIAL PRIMARY KEY,
  post_id BIGSERIAL NOT NULL,
  user_id BIGSERIAL NOT NULL,      
  comment TEXT NOT NULL,
  created_at timestamp with time zone,
  updated_at timestamp with time zone,
  CONSTRAINT comment_post FOREIGN KEY (post_id) REFERENCES post,
  CONSTRAINT comment_user FOREIGN KEY (user_id) REFERENCES app_user
);

 

 

5. Entity 작성

User

@Entity
@Table(name = "app_user")
class UserEntity (

    @Column (name = "email", nullable = false)
    val email: String,

    @Column (name = "password", nullable = false)
    val password: String,

    @Enumerated(EnumType.STRING)
    @Column (name = "role", nullable = false)
    val role: Role,

    @Embedded
    var profile: Profile

){
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null
}
enum class RoleEntity {
    USER,
    ADMIN
}

Post

@Entity
@Table(name = "post")
class PostEntity (

    @Column(name = "title", nullable = false)
    var title: String,

    @Column(name = "description", nullable = false)
    var description: String,

    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    var status: PostStatus,

    @Column(name = "post_image_url", nullable = false)
    var postImageUrl: String,

    @OneToMany(mappedBy = "post", cascade = [CascadeType.ALL])
    var comments: MutableList<CommentEntity> = mutableListOf(),

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    var user: UserEntity

    ){
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null

    @Column(name = "created_at")
    val createdAt: LocalDateTime? = LocalDateTime.now()

    @Column(name = "updated_at")
    var updatedAt: LocalDateTime? = LocalDateTime.now()
}
enum class PostStatusEntity {
    UNCOMPLETE,
    COMPLETE
}

Comment

@Entity
@Table(name = "comment")
class CommentEntity (

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    val post: PostEntity,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    val user: UserEntity,

    @Column(name = "comment", nullable = false)
    var comment: String,

){
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null

    @Column(name = "created_at")
    val createdAt: LocalDateTime? = LocalDateTime.now()

    @Column(name = "updated_at")
    var updatedAt: LocalDateTime? = LocalDateTime.now()
}

 

 

6. 게시판 CRUD, 댓글 CRUD, login/join 기능 까지 만들고 과제 진행!!

 


 

🎀 주특기 플러스 주차 복습과제

💬 1/30 - 회원 가입 API

  • [ ⭕ ] 닉네임, 비밀번호, 비밀번호 확인을 request에서 전달받기
  • [ ⭕ ] 닉네임은 최소 3자 이상, 알파벳 대소문자(a~z, A~Z), 숫자(0~9)로 구성하기
  • [ ] 비밀번호는 최소 4자 이상이며, 닉네임과 같은 값이 포함된 경우 회원가입에 실패로 만들기
  • [ ⭕ ] 비밀번호 확인은 비밀번호와 정확하게 일치하기
  • [ ⭕ ] 데이터베이스에 존재하는 닉네임을 입력한 채 회원가입 버튼을 누른 경우 "중복된 닉네임입니다." 라는 에러메세지를 response에 포함하기
  • [ ⭕ ] 회원 가입 버튼을 누르기 전, 같은 닉네임이 존재하는지 "확인" 버튼을 눌러 먼저 유효성 검증부터 할 수 있도록 해보기
  • [ ⭕ ] (챌린지 과제) 데이터베이스에 비밀번호를 평문으로 저장하는 것이 아닌, 단방향 암호화 알고리즘을 이용하여 암호화 해서 저장하도록 하기
  • [ ⭕ ] (챌린지 과제) 회원 가입 시, 이메일 혹은 SNS로 인증 번호를 전달 받고 5분 이내에 해당 인증 번호를 검증해야 회원 가입에 성공하도록 해보기 (redis TTL 특징을 좀 더 파악하기 위함.)

💬 1/31 - 로그인 API

  • [ ⭕ ] 닉네임, 비밀번호를 request에서 전달받기
  • [ ⭕ ] 로그인 버튼을 누른 경우 닉네임과 비밀번호가 데이터베이스에 등록됐는지 확인한 뒤, 하나라도 맞지 않는 정보가 있다면 "닉네임 또는 패스워드를 확인해주세요."라는 에러 메세지를 response에 포함하기
  • [ ⭕ ] 로그인 성공 시, 로그인에 성공한 유저의 정보를 JWT를 활용하여 클라이언트에게 Cookie로 전달하기

💬 2/1 - 전체 게시글 목록 조회 API

  • [ ⭕ ] 제목, 작성자명(nickname), 작성 날짜를 조회하기
  • [ ⭕ ] 작성 날짜 기준으로 내림차순 정렬하기
  • [ ⭕ ] (챌린지 과제) 전체 조회가 아닌 페이징 조회를 할 수 있도록 해보기
  • [ ⭕ ] (챌린지 과제) 페이징 + 커스텀 정렬 기능 구현하기 -> 사용자가 입력한 key와 정렬 기준을 동적으로 입력 받아, 해당 기준에 맞게 데이터를 제공. (예. 작성자명 오름차순 정렬 and 작성 날짜 오름차순 정렬된 결과를 상위 5개만 출력)

💬 2/2 - 게시글 작성 API

  • [ ⭕ ] 토큰을 검사하여, 유효한 토큰일 경우에만 게시글 작성 가능
  • [ ⭕ ] 제목(500자 까지 입력 가능), 작성 내용을 입력하기(5000자 까지 입력 가능)
  • [ ⭕ ] (챌린지 과제) 이미지 업로드 가능

💬 2/5 - 게시글 조회 API

  • [ ⭕ ] 제목, 작성자명(nickname), 작성 날짜, 작성 내용을 조회하기 (검색 기능이 아닙니다. 간단한 게시글 조회만 구현해주세요.)

💬 2/6 - 게시글 수정 API

  • [ ⭕ ] 토큰을 검사하여, 해당 사용자가 작성한 게시글만 수정 가능

💬 2/7 - 게시글 삭제 API

  • [ ⭕ ] 토큰을 검사하여, 해당 사용자가 작성한 게시글만 삭제 가능
  • [ ] (챌린지 과제) 수정된지 90일이 지난 데이터는 자동으로 지우는 스케줄러 기능을 개발해보기. (데이터 삭제 및 백업도 굉장히 중요한 기능)
    • [ ] 스케줄러에 대한 가이드라인은 별도로 제공하지 말 것. (Spring Scheduler를 쓰던, 크론잡을 쓰던 선택지를 다양하게 줄 것.)
    • [ ] 90일이라고 하는 스펙은 알아서 정하기. (다만, 그 이유를 적기)
      • [ ] UTC의 스케줄러가 동작하는 현재 일시 (2023-12-11T02:11:23) 기준으로 90일이 지난 데이터를 지운다.
      • [ ] UTC의 스케줄러가 동작하는 현재 날짜 (2023-12-10) 기준으로 90일이 지난 데이터를 지운다.
      • [ ] LocalTime(+09:00)의 스케줄러가 동작하는 현재 일시 (2023-12-11T11:11:23) 기준으로 90일이 지난 데이터를 지운다.
      • [ ] LocalTime(+09:00)의 스케줄러가 동작하는 현재 일시 (2023-12-11) 기준으로 90일이 지난 데이터를 지운다.

💬 2/8 - 댓글 작성 API

  • [ ⭕ ] 게시글과 연관 관계를 가진 댓글 테이블 추가
  • [ ⭕ ] 토큰을 검사하여, 유효한 토큰일 경우에만 게시글 작성 가능
  • [ ⭕ ] 작성 내용을 입력하기
  • [ ⭕ ] 게시글에 대한 좋아요

💬 2/13 - 게시글과 댓글 목록 조회 API, 댓글 수정/삭제 API

  • [ ⭕ ] 댓글 목록 조회
    • [ ⭕ ] (챌린지 과제) 전체 조회가 아닌 페이징 조회를 할 수 있도록 해보기
    • [ ⭕ ] (챌린지 과제) 페이징 + 커스텀 정렬 기능 구현하기 -> 사용자가 입력한 key와 정렬 기준을 동적으로 입력 받아, 해당 기준에 맞게 데이터를 제공. (예. 작성자명 오름차순 정렬 and 작성 날짜 오름차순 정렬된 결과를 상위 5개만 출력)
  • [ ⭕ ] 게시글 조회 API 호출시 해당 게시글의 댓글 목록도 응답
  • [ ⭕ ] 토큰을 검사하여, 해당 사용자가 작성한 댓글만 수정/삭제 가능
    • [ ] (챌린지 과제) 게시글이 삭제될 때 연관된 댓글도 같이 지우도록 스케줄러 코드 기능 추가

 


 

 

🎀 고려해 볼 만한 코드 개선 방안

📌 1. Controller, Service 패키지 내 클래스 개선

  • 1/30 -  Controller, Service 패키지 내 클래스 개선
    1. Controller Advice 로 예외 공통화 처리하기
    [ ⭕ ]  @RestControllerAdvice 를 사용해서 모든 RestController 의 예외를 공통화해서 처리해주세요!
    2. Service 인터페이스와 구현체 분리하여 추상화 하기
    [ ⭕ ]  Service 클래스를 인터페스와 구현체로 분리하고 인터페이스 메서드에 주석을 친절하게 달아주세요!

  • 1/31 - CustomException 정의 및 SpringAOP 적용
    1. CustomException 정의
    애플리케이션에서 발생하는 예외상황은 정말 다양한데 기존에 존재하는 Exception 만 가지고 구현하려니 힘드셨죠?
    [ ⭕ ]  이제는 `RuntimeException`을 상속 받아서 CustomException 을 상황에 맞게 만들어보세요.
    2. Spring AOP 적용
    [ ]  Spring AOP 를 사용하여 부가기능을 추가해보세요.

 

📌 2. JPA 심화 기술을 사용하여 검색기능 고도화

  • 2/1 - QueryDSL 을 사용하여 검색 기능 만들기
    [ ⭕ ]  QueryDSL 의 jpaQueryFactory 를 사용해서 검색기능을 만들어주세요!

  • 2/2 - Pageable 을 사용하여 페이징 및 정렬 기능 만들기
    [ ⭕ ]  Pageable 을 사용해서 원하는 페이지 사이즈만큼만 조회 해주세요! (JpaRepository, QueryDSL 모두)

  • 2/5 - 다양한 조건을 동적 쿼리로 처리하기
    [ ]  아래 조건을 만족하는 동적 쿼리를 작성해 주세요!
        - 제목 (포함)
        - 태그 (포함)
        - 카테고리 (정확히 일치)
        - 게시글 상태 (정확히 일치)
        - N일전 게시글
    (포함) 조건은 주어진 텍스트가 값에 포함되어있다면 조회하고,
    (정확히 일치) 조건은 말 그대로 값이 정확히 일치해야 조회한다는 뜻입니다.

 

📌 3. 코드를 체크할 수 있는 테스트 코드 작성

  •  2/6 - Controller 테스트 코드 작성하기
    [ ⭕ ]  MockMvc 를 사용해서 Controller 테스트 코드를 작성해주세요!

  • 2/7 - Service 테스트 코드 작성하기
    [ ⭕ ]  Mockito 을 사용하여 Service 테스트 코드를 작성해주세요!

  • 2/8 -Repository 테스트 코드 작성하기
    [ ]  @DataJpaTest 를 사용해서 Repository 테스트 코드를 작성해주세요!

 

📌 4. AWS 를 활용한 기능 추가 및 배포

  • 2/13 AWS S3 를 이용해 이미지 업로드 기능 구현하기
    [ ]  AWS IAM, S3 CORS, .env 파일설정 후 이미지 업로드 기능 구현해주세요!
    [ ]  AWS EC2 를 이용해 애플리케이션 .jar 파일 배포하기

 

블로그의 정보

꿈틀꿈틀 개발일기

jeongminy

활동하기