본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
1장 : 회원가입과 게시판 시작

회원가입·게시판 첫 흐름

첫 실행부터 회원가입, 로그인 화면, 게시글 작성·조회까지 요청이 이동하는 길을 연결하고 이후 장에서 확장할 책임을 정리합니다.

앞 문서까지 프로젝트 실행, 요청과 응답, 게시글 모델, 저장소, 서비스, 컨트롤러를 차례로 만들었습니다. 이번 문서에서는 새 기술을 더하지 않고 지금까지 만든 조각을 사용자가 실제로 밟는 순서로 연결합니다.

처음 배우는 단계에서는 다음 네 단어만 구분하면 됩니다.

  • 요청: 브라우저가 서버에 보내는 URL, 메서드, 입력값
  • 컨트롤러: 요청을 읽고 알맞은 사용 사례를 호출하는 객체
  • 서비스: 회원가입이나 게시글 작성 같은 업무 순서를 수행하는 객체
  • 리포지토리: 회원과 게시글을 저장하고 다시 찾는 역할

사용자가 경험하는 순서

첫 버전의 화면과 URL은 다음과 같습니다.

순서화면요청서버가 하는 일
1GET /회원가입과 게시글 목록 링크를 보여 준다
2회원가입GET /members/new빈 가입 폼을 보여 준다
3회원가입 처리POST /members입력을 검증하고 회원을 저장한다
4게시글 작성POST /posts로그인 회원을 작성자로 하여 글을 저장한다
5게시글 상세GET /posts/{postId}저장된 글을 찾아 화면에 보여 준다

URL은 화면의 파일명이 아니라 사용자가 다루는 자원을 나타냅니다. /members는 회원 모음, /posts는 게시글 모음, /posts/1은 식별자가 1인 게시글입니다.

회원가입 입력

비밀번호 원문을 회원 객체에 저장하면 안 됩니다. 웹 입력 객체는 원문을 잠시 받지만 서비스가 PasswordHasher를 호출한 뒤에는 해시만 회원에 넣습니다. 해시는 원문을 그대로 복원하기 위한 암호화가 아니라 비밀번호 일치 여부를 안전하게 확인하기 위한 변환 결과입니다.

src/main/java/board/member/SignUpRequest.java
package board.member;

public record SignUpRequest(
        String email,
        String name,
        String rawPassword
) { }
src/main/java/board/member/MemberRegistrationService.java
package board.member;

public final class MemberRegistrationService {
    private final MemberRepository members;
    private final PasswordHasher passwordHasher;

    public MemberRegistrationService(
            MemberRepository members,
            PasswordHasher passwordHasher
    ) {
        this.members = members;
        this.passwordHasher = passwordHasher;
    }

    public Member register(SignUpRequest request) {
        if (members.existsByEmail(request.email())) {
            throw new DuplicateEmailException(request.email());
        }
        String passwordHash = passwordHasher.hash(request.rawPassword());
        return members.save(Member.signUp(
                request.email(), request.name(), passwordHash));
    }
}

컨트롤러는 비밀번호 해시 방법이나 저장 자료구조를 알지 않습니다. 서비스도 HTTP 요청 객체를 알지 않습니다. 각 객체가 자기 역할에 필요한 정보만 알게 하는 것이 이후 변경을 작게 만드는 출발점입니다.

게시글 작성 입력

게시글은 작성자, 제목, 본문을 가집니다. 브라우저가 보낸 작성자 ID를 그대로 믿지 않고 로그인 상태에서 확인한 회원 ID를 사용해야 합니다.

src/main/java/board/post/CreatePostRequest.java
package board.post;

public record CreatePostRequest(String title, String content) { }
src/main/java/board/post/PostService.java
package board.post;

public final class PostService {
    private final PostRepository posts;

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

    public Post create(long authorId, CreatePostRequest request) {
        var draft = PostDraft.create(
                authorId, request.title(), request.content());
        return posts.save(draft);
    }

    public Post find(long postId) {
        return posts.findById(postId)
                .orElseThrow(() -> new PostNotFoundException(postId));
    }
}

PostDraft는 아직 식별자가 없는 작성 요청이고 Post는 저장소가 식별자를 부여한 결과입니다. 제목과 본문 검증은 두 진입점이 같은 규칙을 쓰도록 도메인 객체에서도 지킵니다.

요청 하나의 이동 경로

POST /posts가 들어오면 다음 순서로 이동합니다.

  1. Spring MVC가 폼 또는 JSON을 CreatePostRequest로 바꾼다.
  2. 컨트롤러가 로그인 회원 ID와 요청 값을 PostService에 전달한다.
  3. 서비스가 PostDraft를 만들고 PostRepository.save를 호출한다.
  4. 저장소가 식별자를 부여한 Post를 반환한다.
  5. 컨트롤러가 상세 주소 /posts/{id}로 이동시키거나 JSON 응답을 만든다.

아직 다루지 않은 것

첫 장에서는 전체 흐름을 읽을 수 있을 만큼만 구현했습니다. 다음 기술은 이름만 확인하고 뒤 장에서 정의부터 다시 배웁니다.

  • 데이터베이스 저장과 트랜잭션: 9~11장
  • 프록시와 스레드별 요청 상태: 12장
  • 로깅 같은 공통 기능을 분리하는 AOP: 13장
  • Spring Boot 자동 구성과 외부 설정: 14장
  • 상태 확인과 메트릭: 15장

따라서 지금 JDBC, 트랜잭션, 프록시, AOP를 몰라도 다음 장으로 진행할 수 있습니다. 첫 장의 완료 기준은 다음 세 문장을 자기 말로 설명하는 것입니다.

  • 브라우저 요청은 컨트롤러를 거쳐 서비스로 전달된다.
  • 서비스는 저장 방법이 아니라 회원가입과 게시글 작성 순서를 담당한다.
  • 리포지토리는 메모리든 데이터베이스든 같은 저장 역할을 제공한다.

직접 확인하기

애플리케이션을 실행한 뒤 홈, 회원가입, 게시글 작성, 게시글 상세 순서로 이동합니다. 실패하면 한꺼번에 추측하지 말고 다음 경계를 차례로 확인합니다.

  1. 요청 URL과 HTTP 메서드가 컨트롤러 매핑과 같은가?
  2. 입력값이 요청 객체로 변환됐는가?
  3. 서비스가 기대한 저장소와 해시 구현을 주입받았는가?
  4. 저장 결과의 식별자로 상세 URL을 만들었는가?

다음 장에서는 이 객체들을 Spring 없이 직접 조립해 봅니다. 그래야 Spring 컨테이너가 무엇을 대신해 주는지 보입니다.