풀스택 개발자 과정 31일차

너구·2026년 6월 22일

풀스택 성장과정

목록 보기
31/79

MemberController.java

package com.example.boardapp.controller;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.boardapp.domain.Member;
import com.example.boardapp.dto.SignUpRequest;
import com.example.boardapp.mapper.MemberMapper;

import jakarta.validation.Valid;

@Controller // 컨트롤러 빈으로 등록
@RequestMapping("/member") // URL prefix: 이 컨트롤러가 다루는 모든 URL 앞에 이 값을 붙인다
public class MemberController { // 멤버 관련된 요청/응답을 처리하는 컨트롤러

    // 의존성 선언
    private final MemberMapper memberMapper; // 멤버 데이터 다루는 용도
    private final PasswordEncoder passwordEncoder; // 비밀번호 암호화하는 객체

    public MemberController(MemberMapper memberMapper, PasswordEncoder passwordEncoder) {
        this.memberMapper = memberMapper;
        this.passwordEncoder = passwordEncoder;
    }
    
    // 회원가입 페이지 요청을 처리하는 핸들러(처리하는 사람)
    @GetMapping("/join") // GET /join 요청을 처리한다
    // Model: MVC(Model-View-Controller)에서 모델에 해당한다
    // 모델의 역할: 데이터를 활용해 뷰를 업데이트한다
    public String joinFrom(Model model) {
        
        // 뷰의 memberForm과 가입요청(SignUpRequest)를 매핑한다
        // 사용자가 가입할 때 입력한 값이 이 DTO에 저장된다
        model.addAttribute("memberForm", new SignUpRequest());

        // 회원가입 페이지를 클라이언트에게 전송한다.
        return "member/join"; // 나중에 이 페이지를 만들거다
    }

    // 회원가입 처리 핸들러
    @PostMapping("/join") // 요청을 처리할 주소. POST(데이터 생성 요청) /join
    // dto 매개변수 앞에 두 개의 어노테이선 @Valid, @ModelAttribute가 붙은 상태
    // @Valid: dto를 대상으로 사전에 정의한 유효성 검사를 실행하라
    // @ModelAttribute: 가입 페이지의 memberForm과 가입요청 dto를 매핑한다
    // BindingResult: validation이 가입요청DTO(SignUpRequest)를 대상으로 
    // 유효성 검증을 마치고 결과를 담은 객체
    public String join(@Valid @ModelAttribute("memberForm") SignUpRequest dto, BindingResult
    bindingResult) {

        // 비밀번호와 비밀번호 확인(password confirm)이 일치하지 않을 때
        if (!dto.getPassword().equals(dto.getPasswordConfirm())) {
            // 이 경우 bindingResult에 직접 에러를 추가한다.
            // bindingResult에 에러를 수집하는 중
            // rejectValue(필드, 에러코드, 에러 메시지)
            bindingResult.rejectValue("getPasswordConfirm", "passwordMismatch",
            "비밀번호가 일치하지 않습니다");
        }

        // 사용자가 입력한 유저네임이 데이터베이스에 존재하는지 검사
        // dto.getUsername(): 사용자가 가입할 때 입력한 유저네임(아이디)
        // isPresent: 옵셔널의 메서드. 내부에 값이 존재하는지 확인한다 (boolean 타입으로 결과 반환)
        if (memberMapper.findByUsername(dto.getUsername()).isPresent()) {
            // 아이디 중복 오류를 bindingResult에 새롭게 추가한다
            bindingResult.rejectValue("username", "duplicatedUsername",
            "이미 존재하는 아이디입니다");
        }

        // bindingResult에 에러가 있다면
        // 에러: validation이 추가한 에러, 우리가 직접 추가한 에러 등
        if (bindingResult.hasErrors()) {
            // 에러메시지와 함께 가입페이지를 다시 전송한다
            return "member/join";
        }

        // 모든 검사를 통과한 경우 가입 절차를 시작한다.

        // 멤버 객체를 생성한다 
        // 비밀번호를 암호화해서 저장한다
        Member member = new Member(dto.getUsername(), passwordEncoder.encode(dto.getPassword()));

        // 데이터베이스에 저장한다
        memberMapper.save(member);

        // 로그인 요청 핸들러로 이동한다
        return "redirect:/member/login";
    }

    // 로그인 페이지 요청 핸들러
    @GetMapping("/login")
    public String loginForm() {
        // 클라이언트에게 로그인 페이지를 전송한다
        return "member/login";
    }
}

MemberController는 회원(Member)과 관련된 요청을 처리하는 컨트롤러이다.
사용자의 회원가입 및 로그인 요청을 받아 처리하며 회원가입 페이지를 화면에 표시하거나 사용자가 입력한 회원 정보를 검증하는 역할을 한다.
또한 MemberMapper를 통해 데이터베이스와 통신하고 PasswordEncoder를 이용해 비밀번호를 암호화하여 안전하게 저장할 수 있도록 한다.
Spring MVC의 Model 객체를 활용하여 화면(View)에 필요한 데이터를 전달한다.

PostController.java

package com.example.boardapp.controller;

import java.security.Principal;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.boardapp.domain.Member;
import com.example.boardapp.domain.Post;
import com.example.boardapp.dto.PostDetailsResponse;
import com.example.boardapp.dto.PostListResponse;
import com.example.boardapp.dto.PostWriteRequest;
import com.example.boardapp.mapper.MemberMapper;
import com.example.boardapp.mapper.PostMapper;

import jakarta.validation.Valid;

@Controller // 컨트롤러 빈으로 등록한다
@RequestMapping("/posts") // URL prefix: 이 컨트롤러가 처리하는 URL 앞에 공통적으로 붙인다
public class PostController {
    
    // 의존성 선언하기
    private final PostMapper postMapper; // 게시물 데이터 처리
    private final MemberMapper memberMapper; // 멤버 데이터 처리

    public PostController(PostMapper postMapper, MemberMapper memberMapper) {
        this.postMapper = postMapper;
        this.memberMapper = memberMapper;
    }

    // 글 작성 페이지 요청 핸들러
    @GetMapping("/write") // GET /write 요청을 처리한다
    public String writeForm(Model model) {

        // 뷰의 postForm(게시물 작성폼)과 게시물 생성 DTO(PostwriteRequest)를 매핑한다
        model.addAttribute("postForm", new PostWriteRequest());
        
        // 서버의 응답
        return "post/write"; // 게시물 작성 페이지를 전송한다
    }

    // 글 작성을 처리하는 핸들러
    @PostMapping // Post /prefix(/post)
    // dto 앞에 @valid, @ModelAttribute 어노테이션이 붙은 상태
    // @valid: 사전에 정의한 유효성 검사를 실행하라
    // @ModelAttribute: postForm(게시물 작성폼)과 dto를 매핑한다.
    // BindingResult: validation이 유효성 검사를 수행하고 결과를 담은 객체
    public String write(@Valid @ModelAttribute("postForm") PostWriteRequest dto, BindingResult
    bindingResult, Principal principal) {
        
        // 게시물 유효성 검사를 통과하지 못한 경우 예) 사용자가 글 제목이나 본문을 입력 안하고 전송한 경우
        if (bindingResult.hasErrors()) {
            // 게시물 작성페이지를 전송한다. 오류 메시지와 함께
            return "post/write";
        }

        // 로그인한 멤버를 확인한다 (데이터 베이스 저장용)
        // principal.getName(): 로그인 중인 사용자의 유저네임을 반환
        Member loginMember = memberMapper.findByUsername(principal.getName())
            .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다"));
        
        // 게시물을 작성자 정보와 함께 저장한다
        Post post = new Post(dto.getTitle(), dto.getContent(), loginMember);
        postMapper.save(post);

        // 방금 작성한 글 보기 페이지로 이동
        return "redirect:/posts/details?id=" + post.getId();
        // .xml의 useGenerateKey=true를 활용하는 부분이 여기이다
        // 게시물 생성 후 바로 id에 접근 가능
    }

    // 게시물 목록 페이지 요청 핸들러
    @GetMapping // GET /prefix 요청을 처리한다
    public String list(Model model) {

        // 데이터베이스로부터 가져온 게시물들로부터 글 목록 DTO(PostListResponse)를 생성하는 과정
        List<PostListResponse> posts = postMapper.findAll().stream()
            .map((post) -> {
                PostListResponse dto = new PostListResponse();
                dto.setId(post.getId());
                dto.setTitle(post.getTitle());
                // 멤버 정보 중 이름만 가져온다
                dto.setMemberName(post.getMember().getUsername());
                return dto;
            })
            .collect(Collectors.toList());

        // 뷰에게 dto를 전달한다
        // posts(attributeName): 뷰에서 이 dto를 저장하는 부분
        // 뷰는 이 데이터를 바탕으로 화면을 완성한다
        model.addAttribute("posts", posts);

        // 서버의 응답 (글 목록 페이지를 전송한다)
        return "post/list";
    }

    // 게시물 상세보기 페이지
    @GetMapping("/details")
    // id 매개변서 앞에 @RequestParam 어노테이션이 붙은 상태
    // @RequestParam: 요청-매개변수를 의미한다
    // 이 요청 매개변수에 사용자가 전송한 게시물의 id가 저장되어있다. 예) /details?id=99
    // 주소?요청 매개변수 키=값
    public String details(@RequestParam("id") Long id, Model model) {

        // id에 해당하는 게시물을 검색
        Post post = postMapper.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 게시물입니다"));

        // 게시물로부터 글 상세보기 DTO(PostDetailsResponse)를 생성
        PostDetailsResponse dto = new PostDetailsResponse();
        dto.setId(post.getId());
        dto.setTitle(post.getTitle());
        dto.setContent(post.getContent());
        dto.setMemberName(post.getMember().getUsername());

        // 모델이 뷰에게 dto를 전달한다. 뷰는 화면을 이제 완성할 수 있다.
        // 뷰의 'post' 자리에 이 dto가 들어온다
        model.addAttribute("post", dto);

        // 서버의 응답 (글 상세페이지 전송)
        return "post/details";
    }

    // 게시물 삭제 요청 핸들러
    @PostMapping("/delete")
    // id: 사용자가 삭제를 요청한 게시물의 id
    // Principal: 현재 로그인 중인 사용자 정보
    public String remove(@RequestParam Long id, Principal principal) {

        // 삭제할 게시물을 검색한다
        Post post = postMapper.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글: " + id));
        // 예외를 던지면 예외처리기에서 400에러로 처리를 할 것

        // 로그인한 멤버를 확인한다
        Member loginMember = memberMapper.findByUsername((principal.getName()))
            .orElseThrow(() -> new IllegalArgumentException("사용자 정보가 존재하지 않습니다"));

        // 삭제를 요청한 사용자가 이 게시물의 작성자인지 확인
        if (post.getMember().getId().equals(loginMember.getId())) {
            // 게시물을 삭제
            postMapper.delete(id);
        }

        // 게시물 목록 페이지로 돌아간다
        return "redirect:/posts";
    }
}

PostController는 게시글(Post)과 관련된 요청을 처리하는 컨트롤러이다.
게시글 목록 조회, 게시글 상세 조회, 게시글 작성 및 삭제 등의 기능을 담당한다.
사용자의 요청을 받아 PostMapper를 통해 데이터베이스와 통신하고 필요한 데이터를 DTO로 변환하여 화면(View)에 전달한다.
또한 Model 객체를 활용해 게시글 정보를 화면에 출력할 수 있도록 처리한다.


그 뒤 선생님께 받은 templates 파일을 넣고 실행시키면 로그인 화면과 회원가입 그리고 여러가지 구성 화면들이 나오는 것을 확인해 볼 수 있다.


마무리

힘들다...

0개의 댓글