본문 바로가기
TIL

[231103] Spring 개인과제

by 진진리 2023. 11. 3.
728x90

Spring 입문 강의 2주차를 마무리하고 주어진 개인과제를 했다.

강의에서 배우자마자 과제를 하다보니 내가 잘 알아서 코드를 짠다기 보다 얼추 끼워 맞춰가면서 과제를 하는 느낌이었다.

거의 강의 자료를 토대로 따라한 거긴 하지만 일단 개인과제를 다 했기 때문에 기록해두려고 한다.


과제: 스파르타 익명 게시판 서버 만들기

필수 구현 기능
  1. 게시글 작성 기능
    • 제목, 작성자명, 비밀번호, 작성 내용, 작성일을 저장할 수 있습니다.
    • 저장된 게시글의 정보를 반환 받아 확인할 수 있습니다.
      • 반환 받은 게시글의 정보에 비밀번호는 제외 되어있습니다.
  2. 선택한 게시글 조회 기능
    • 선택한 게시글의 정보를 조회할 수 있습니다.
      • 반환 받은 게시글의 정보에 비밀번호는 제외 되어있습니다.
  3. 게시글 목록 조회 기능
    • 등록된 게시글 전체를 조회할 수 있습니다.
      • 반환 받은 게시글의 정보에 비밀번호는 제외 되어있습니다.
    • 조회된 게시글 목록은 작성일 기준 내림차순으로 정렬 되어있습니다.
  4. 선택한 게시글 수정 기능
    • 선택한 게시글의 제목, 작성자명, 작성 내용을 수정할 수 있습니다.
      • 서버에 게시글 수정을 요청할 때 비밀번호를 함께 전달합니다.
      • 선택한 게시글의 비밀번호와 요청할 때 함께 보낸 비밀번호가 일치할 경우에만 수정이 가능합니다.
    • 수정된 게시글의 정보를 반환 받아 확인할 수 있습니다.
      • 반환 받은 게시글의 정보에 비밀번호는 제외 되어있습니다.
  5. 선택한 게시글 삭제 기능
    • 선택한 게시글을 삭제할 수 있습니다.
      • 서버에 게시글 삭제를 요청할 때 비밀번호를 함께 전달합니다.
      • 선택한 게시글의 비밀번호와 요청할 때 함께 보낸 비밀번호가 일치할 경우에만 삭제가 가능합니다.

 


프로젝트 설계

Use Case Diagram 그리기

 

학교 다닐 때 유스케이스 다이어그램을 그려본 적이 있긴 하지만 잘 생각이 나지 않아서 검색을 해본 뒤 그려보았다.

우선 필수 요구사항으로 주어진 5가지 기능을 사용자가 수행할 수 있다.

게시글 수정과 삭제를 위해서는 비밀번호 확인이 반드시 포함되어야 하며

비밀번호가 불일치하는 경우에는 에러가 발생한다.

게시판 서버는 모든 기능과 다 연결되어 있기 때문에 복잡해질 것 같아 일부로 선으로 연결하지 않았다.

 

API 명세서 작성하기
기능 Method URL request response
게시글 작성 POST /api/post
{
    "title""post_title4",
    "username" : "username4",
    "pw" : 1234,
    "contents" : "contents4"
}
등록한 게시글 정보
(비밀번호 제외)
{
    "postId"4,
    "title""post_title4",
    "username""username4",
    "contents""contents4"
}
게시글 목록 조회 GET /api/post  
등록된 모든 게시글의 정보
(비밀번호 제외)
선택한 게시글 조회 GET /api/post/{postId}  
선택된 게시글의 정보
(비밀번호 제외)
{
    "postId"1,
    "title""titleeee",
    "username""username",
    "contents""contents"
}
선택한 게시글 수정 PUT /api/post/{postId}
{
    "title""titleeee",
    "username" : "username",
    "pw" : 1234,
    "contents" : "contents"
}
수정된 게시글의 id
1
선택한 게시글 삭제 PUT /api/post/delete/{postId}
1234
삭제된 게시글의 id
1

 

처음에는 선택한 게시글을 삭제하는 기능을 구현하기 위해 DELETE 메소드를 사용하면 되겠다고 생각했다.

그런데 삭제를 요청할 때 비밀번호를 함께 전달해야 하는데 찾아보니 DELETE 메소드는 Body 값이 비워져있다고 한다.

 

그리고 비밀번호를 URL로 전달하기에는 보안상 적절하지 못한 것 같아서 PUT 메소드를 사용해 body 내부에 비밀번호 데이터를 담아 request를 받는 것으로 설계하였다.

그러다보니 처음에는 수정과 삭제 모두 URL을 /api/post/{postId}로 지정하였는데 PUT 메소드가 겹치다보니

삭제 기능에서는 url에 delete를 추가하였다.

 

ERD 작성하기

 

스프링을 다뤄보는 것도 처음인데 ERD도 처음 작성하다보니 어떻게 그리면 좋을지 난감했다.

주어진 요구사항도 게시글만을 다루고 있어서 Entity로 게시글밖에 떠오르지 않았다.

Relation을 생각하다보니 처음에는 Entity로 User와 Post를 생각하고

한 사용자가 여러 개의 게시글을 가질 수 있다는 관계를 떠올렸다.

 

사용자 테이블을 따로 만들고 사용자id를 FK로 사용하는 방식으로 구현하면 어떨까 고민하면서

이렇게 하면 유지보수성 면에서도 나중에 고객 데이터를 추가해서 관리하기 좋을 것 같다는 생각을 했다.

 

그런데 구현하고자 하는 과제가 '익명' 게시판이다보니 결국은 고객 데이터가 필요 없다는 생각이 들었다.

익명 게시판이다 보니 같은 사람이 글을 쓸 때마다 다른 이름을 등록할 수 있기 때문이다.

 

결국은 다른 Entity는 떠올리지 못해서 Post만 남았다.


환경 설정

스프링 강의에서는 순차적으로 다양한 기술들을 사용해나가면서 진행되어서

강의를 마치자마자 과제를 마주하게 되자 환경부터 어떻게 세팅하면 좋을지 난감했다.

 

일단 내가 했던 것들을 기억나는 대로 조금 정리해보자면 이렇다.

 

1. MySQL 데이터베이스 설정 : MySQL에 post라는 데이터베이스를 새로 만들었다.

 

2. 인텔리제이에서 스프링 프로젝트 생성 : Spring web, lombok 연결

 

3. resources > application.properties 에 DB 세팅

spring.datasource.url=jdbc:mysql://localhost:3306/post
spring.datasource.username=root
spring.datasource.password={비밀번호}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

 

4. build.gradle에 JPA 및 MySQL 연결 설정

    // JPA 설정
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	// MySQL
	implementation 'mysql:mysql-connector-java:8.0.28'

 

5. 생성한 프로젝트에 post 데이터베이스 연결


프로젝트 구현

우선 Entity, Controller, Serive, Repository, dto 패키지를 생성하였다.

  • Entity 패키지
    • Post 클래스: id, 게시글 제목, 작성자, 비밀번호, 내용, 작성날짜를 필드로 가진다.
    • Timestamped 추상 클래스
    • Post 클래스가 Timestamped 클래스를 상속받아 createdAt 필드를 추가로 가지도록 하였다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {

    @CreatedDate
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;

}

 

@Entity
@Getter
@Setter
@Table(name = "post")
@NoArgsConstructor
public class Post extends Timestamped {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long postId;

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name = "username", nullable = false)
    private String username;

    @Column(name = "pw", nullable = false)
    private Long pw;

    @Column(name = "contents", nullable = false)
    private String contents;

    public Post(PostRequestDto requestDto) {
        this.title = requestDto.getTitle();
        this.username = requestDto.getUsername();
        this.pw = requestDto.getPw();
        this.contents = requestDto.getContents();
    }

    public void update(PostRequestDto requestDto){
        this.title = requestDto.getTitle();
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
    }
}

 

  • dto 패키지
    • PostRequestDto: 사용자로부터 처음에 입력받는 게시글 제목, 작성자, 비밀번호, 내용을 필드로 가진다.
    • PostResponseDto: Post 클래스에서 비밀번호와 작성일을 제외한 나머지를 필드로 가진다.
@Getter
public class PostRequestDto {
    private String title;
    private String username;
    private Long pw;
    private String contents;
}

 

@Getter
public class PostResponseDto {
    private Long postId;
    private String title;
    private String username;
    private String contents;

    public PostResponseDto(Post post){
        this.postId = post.getPostId();
        this.title = post.getTitle();
        this.username = post.getUsername();
        this.contents = post.getContents();
    }
}

 

  • Controller 패키지
    • PostController 클래스: requestDto 등의 데이터를 입력받아 responseDto를 반환
@RestController
@RequestMapping("/api")
public class PostController {

    private final PostService postService;

    public PostController(PostService postService) {
        this.postService = postService;
    }

    @PostMapping("/post")
    public PostResponseDto createPost(@RequestBody PostRequestDto requestDto){
        return postService.createPost(requestDto);
    }

    @GetMapping("/post")
    public List<PostResponseDto> getPosts(){
        return postService.getPosts();
    }

    @GetMapping("/post/{postId}")
    public PostResponseDto getPostsByPostId(@PathVariable Long postId){
        return postService.getPostByPostId(postId);
    }

    @PutMapping("/post/{postId}")
    public Long updatePost(@PathVariable Long postId, @RequestBody PostRequestDto requestDto){
        return postService.updatePost(postId, requestDto);
    }

    @PutMapping("/post/delete/{postId}")
    public Long deletePost(@PathVariable Long postId, @RequestBody Long pw){
        return postService.deletePost(postId, pw);
    }

}

 

  • Service 패키지
    • PostService 클래스: 필요한 기능들이 실제로 구현되는 클래스로 postRepository로 데이터를 저장하거나 조회한다.
    • 메서드 findPost(postId) : 수정 및 삭제하고자 하는 게시글이 존재하는지 확인 
    • 메서드 checkPw(post, pw) : 비밀번호가 일치하는지 확인
@Service
public class PostService {

    private final PostRepository postRepository;

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    public PostResponseDto createPost(PostRequestDto requestDto) {
        Post post = new Post(requestDto);

        Post savePost = postRepository.save(post);

        PostResponseDto postResponseDto = new PostResponseDto(post);

        return postResponseDto;
    }

    public List<PostResponseDto> getPosts() {
        return postRepository.findAllByOrderByCreatedAtDesc()
            .stream().map(PostResponseDto::new).toList();
    }

    public PostResponseDto getPostByPostId(Long postId) {
        return new PostResponseDto(postRepository.findByPostId(postId));
    }

    @Transactional
    public Long updatePost(Long postId, PostRequestDto requestDto) {
        //DB에 존재하는지 확인
        Post post = findPost(postId);

        //비밀번호가 일치하는지 확인
        try {
            checkPw(post,requestDto.getPw());
        } catch (IncorrectPwException e){
            System.out.println(e.getMessage());
            return null;
        }

        post.update(requestDto);

        return postId;
    }

    public Long deletePost(Long postId, Long pw) {
        //DB에 존재하는지 확인
        Post post = findPost(postId);

        //비밀번호가 일치하는지 확인
        try {
            checkPw(post,pw);


        } catch (IncorrectPwException e){
            System.out.println(e.getMessage());
            return null;
        }
        postRepository.delete(post);

        return postId;

    }

    public Post findPost(Long postId){
        return postRepository.findById(postId).orElseThrow(() ->
            new IllegalArgumentException("선택한 게시글은 존재하지 않습니다.")
        );
    }

    public void checkPw(Post post, Long pw) throws IncorrectPwException{
        if(!Objects.equals(post.getPw(), pw)){
            throw new IncorrectPwException();
        }
    }
}

 

  • Repository 패키지
    • PostRepository 인터페이스: JpaRepository를 상속받아 메소드명을 통해 쿼리를 생성하여 사용하였다.
    • findAllByOrderByCreatedAtDesc(): 모든 게시글을 생성일 내림차순으로 정렬해 리스트로 반환
    • findByPostId(postId): postId로 찾은 post를 반환
    •  
public interface PostRepository extends JpaRepository<Post, Long> {
    List<Post> findAllByOrderByCreatedAtDesc();

    Post findByPostId(Long postId);
}

 

추가 구현 기능
: 선택한 게시글 수정 및 삭제 요청 시 비밀번호가 일치하지 않을 경우 API 요청 실패(예외상황)에 대해 판단할 수 있는 Status Code, Error 메시지등의 정보를 반환합니다.

 

해당 구현 기능은 어떤 방식으로 Status Code와 Error 메시지 등의 정보를 반환해야 하는지 모르겠어서 구현하지 못했다.

대신 예외처리하여 예외 이유를 print문으로 출력한 후 postId 대신 null 값을 반환하도록 하였다.

에러 메세지를 사용자에게 보여줄 수 있도록 하기 위해서 어떻게 하면 좋은지

해당 기능의 구현 방법을 이후에 보면서 공부해야 겠다.

 

  • IncorrectPwException 클래스
public class IncorrectPwException extends Exception{
    public IncorrectPwException(){
        super("비밀번호가 일치하지 않습니다.");
    }

}

 


깃허브 주소: https://github.com/dlwls423/spring-assignment

 

GitHub - dlwls423/spring-assignment: 내배캠 스프링압문 개인과제

내배캠 스프링압문 개인과제. Contribute to dlwls423/spring-assignment development by creating an account on GitHub.

github.com

 


postman을 이용하여 API 테스트

 

  • 게시글 작성

  • 게시글 목록 조회

  • 선택한 게시글 조회

  • 선택한 게시글 수정

  • 선택한 게시글 삭제

  • post 데이터베이스


고민해보기

 

1. 수정, 삭제 API의 request를 어떤 방식으로 사용하셨나요? (param, query, body)

수정의 경우 입력되는 데이터의 양이 적지 않기 때문에 url이 복잡해질 것 같아 body에 담았다.

삭제의 경우 비밀번호가 url에 노출되는 것을 막기 위해 body에 담았다.

 

2. RESTful한 API를 설계하셨나요?

비밀번호라는 데이터를 받아 비교 후 삭제하려고 할 때 PUT 메소드를 사용했는데 어떤 메소드를 사용하는 것이 적절한 건지 잘 모르겠다.

그리고 url도 어떤 url로 사용해야 좋은 건지도 잘 모르겠다.

처음 설계해본 것이기 때문에 이후 리뷰 시간에 나의 설계와 비교해보면서 배워가야 할 것 같다.

 

3. 적절한 관심사 분리를 적용하셨나요?

강의에서 했던 메모장 프로젝트의 틀을 거의 그대로 가져왔다.

아직은 나 혼자서 잘 분리하지는 못할 것 같다.