풀스택 개발자 과정 30일차

너구·2026년 6월 18일

풀스택 성장과정

목록 보기
30/79

post-mapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--.xml 파일을 호출하는 매퍼(PostMapper)를 정의한다 -->
<mapper namespace="com.example.boardapp.mapper.PostMapper">

<!-- 
findAll: DB로부터 모든 게시물을 조회한다 예) 게시물 목록 페이지
resultMap: 이 쿼리의 결과를 postResultMap에게 전달한다
postResult: 데이터베이스에서 가져온 순수한 데이터(객체 아님)
JOIN과 INNER JOIN은 같다 (OUTER JOIN도 있음)
-->
<select id="findAll" resultMap="postResultMap">
    SELECT p.id AS post_id, p.title, p.content, m.id AS member_id, m.username
    FROM post p INNER JOIN member m ON p.member_id = m.id
    ORDER BY p.id DESC
</select>

<!-- 
resultMap: 쿼리의 결과(result)를 객체와 매핑(Mapping)하는 방법을 정의
resultMap을 활용해 객체 간 연관관계를 정의할 수 있다.
(데이터베이스에서 수행한 조인은 객체 간 연관관계를 의미한다)
type: 매핑되는 객체의 타입
 -->
 <resultMap id="postResultMap" type="com.example.boardapp.domain.Post"><resultMap>
    <!-- post_id 컬럼을 id 속성에 매핑 -->
    <id property="id" column="post_id" />
    <!-- title 컬럼을 title 속서엥 매핑 -->
    <result property="title" column="title" />
    <!-- content 컬럼을 content 속성에 매핑 -->
    <result property="content" column="content" />

    <!-- 
    연관관계 (association)를 처리하는 부분 
    post의 member_id(외래키)Member 객체에 매핑하느 과정
    javaType: 이 객체(post)와 연관된 객체의 자료형. Member-->
    <association property="member" javaType="com.example.boardapp.domain.Member">
    <!-- member_id 컬럼과 id 속성에 매핑 -->
        <id property="id" column="member_id" />
        <!-- username 컬럼과 username 속성에 매핑 -->
        <result property="username" column="username" />
    </association>
 </resultMap>

<!-- 
findById: 입력받는 id에 해당하는 게시물을 찾는다
매개변수 타입이 long(int보다 더 큰 자료형). 데이터베이스의 BIGINT랑 대응
게시물의 개수가 커지는 경우를 생각해서 long 타입을 사용
조인하는 이유: 게시물과 게시물의 작성자를 같이 보고 싶을 때 (대부분)
-->
<select id="findById" parameterType="long" resultMap="postResultMap">
    SELECT p.id AS post_id, p.title, p.content, m.id AS member_id, m.username
    FROM post p INNER FOIN member m ON p.member_id = m.id
    WHERE p.id = #{id}
</select>

<!--
save: 게시물을 DB에 저장하는 메서드
-->
<insert id ="save" parameterType="com.example.boardapp.domain.Post" useGeneratedKeys="true"
keyProperty="id">
    INSERT INTO post (title, content, member_id)
    VALUES (#{title}, #{content}, #{member.id})
</insert>

<!-- delete: 입력받는 id에 해당하는 게시물을 삭제한다-->
<select id="delete" parameterType="long">
    DELETE FROM post WHERE id = #{id}
</delete>
  
</mapper>

domain/ Member.java

package com.example.boardapp.domain;

// 도메인 (Domain): 데이터베이스의 테이블과 매핑되는 객체
// Member 도메인으로 멤버 데이터를 다룰 수 있다.
// 

public class Member {
    
    // 멤버 테이블(DB)과 유사한 구조
    private Long id; // Long/long 둘 다 됨 데이터베이스의 BIGINT에 대응한다
    private String username;
    private String password;

    // 기본 생성자 - 기본적으로 추가하는 것이 좋음
    public Member() {}

    // 멤버 생성용 생성자 (회원가입 등)
    public Member(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // Getter
    public Long getId() {
        return id;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

}

데이터베이스의 회원 데이터를 관리하기 위한 Member 클래스를 구현하였다.

domain/Post

package com.example.boardapp.domain;

// Post 도메인: post 테이블의 데이터를 다룰 때 사용한다

public class Post {
    
    private Long id;
    private String title; // 게시물 제목
    private String content; // 본문
    private Member member; // 게시물 작성자

    // 기본 생성자
    public Post() {}

    // 게시물 생성용 생성자
    public Post(String title, String content, Member member) {
        this.title = title;
        this.content = content;
        this.member = member;
    }

    // Getter
    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public String getContent() {
        return content;
    }

    public Member getMember() {
        return member;
    }
    
}

Post 클래스는 게시글 한 개의 정보를 저장하는 객체로 제목, 내용, 작성자 등의 데이터를 관리하기 위해 작성하였다.

SignUpRequest.java

package com.example.boardapp.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

// 회원가입 요청용 DTO

public class SignUpRequest {
    
    // 사용자 이름의 길이 검증(validation)
    // message: 검증에 실패했을 때 전달할 메시지
    @Size(min = 4, max = 20, message = "아이디는 4자 이상 20자 이하로 입력하세요.")
    private String username;

    // 비밀번호 길이 검증 (최소 6글자)
    @Size(min=6, message = "비밀번호는 최소 6글자 이상이어야 합니다.")
    private String password;

    // NotBlank: 빈 값(black) 안 됨
    // passwordConfirm: 비밀번호 재입력 란의 정보를 저장할 필드
    @NotBlank(message = "비밀번호 재입력은 필수 항목입니다.")
    private String passwordConfirm;

    // 기본 생성자
    public SignUpRequest() {}

    //Getter/Setter: DTO에서는 getter, setter를 보통 함께 사용한다
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getPasswordConfirm() {
        return passwordConfirm;
    }

    public void setPasswordConfirm(String passwordConfirm) {
        this.passwordConfirm = passwordConfirm;
    }

}

SignUpRequest 클래스는 회원가입 시 사용자가 입력한 데이터를 서버로 전달하기 위한 DTO이다.
아이디와 비밀번호 등 회원가입에 필요한 정보를 저장하며 클라이언트와 서버 간 데이터 전달 역할을 수행한다.

PostListResponse.java

package com.example.boardapp.dto;

// 게시물 목록 응답 DTO

public class PostListResponse {
    
    private Long id;
    private String title;
    // 본문은 없음
    private String memberName; // 작성자 유저 네임

    public PostListResponse () {}

    // Getter/Setter

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }    

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }    

    public String getMemberName() {
        return memberName;
    }

    public void setMemberName(String memberName) {
        this.memberName = memberName;
    }
}

PostListResponse 클래스는 게시글 목록 조회 시 필요한 데이터를 전달하기 위한 DTO이다.
게시글 제목, 게시글 번호 등 화면에 필요한 정보만 담아 클라이언트에 전달하는 역할을 수행한다.

PostDeltailsResponse.java

package com.example.boardapp.dto;

// 게시물 상세보기 응답 DTO

public class PostDetailsResponse {
    
    private Long id;
    private String title;
    private String content; // 글 본문 포함
    private String memberName; // 글 작성자 유저네임

    public PostDetailsResponse() {}

    // Getter/Setter
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getMemberName() {
        return memberName;
    }

    public void setMemberName(String memberName) {
        this.memberName = memberName;
    }
    
}

PostDetailsResponse 클래스는 게시글 상세 조회 화면에 필요한 데이터를 전달하기 위한 DTO이다.
게시글 제목, 내용, 작성자 정보 등 상세 화면에 필요한 데이터만 담아 클라이언트에 전달하는 역할을 수행한다.

MemberMapper.java

package com.example.boardapp.mapper;

import java.util.Optional;

import org.apache.ibatis.annotations.Mapper;

import com.example.boardapp.domain.Member;

// 멤버 테이블을 다루는 매퍼
@Mapper // 매퍼로 만들어라
public interface MemberMapper { // 스프링이 내부적으로 인터페이스를 구현해준다
    
    // 멤버 생성하는 메서드
    void save(Member member);

    // 사용자 이름으로 멤버 찾기
    Optional<Member> findByUsername(String username);
    // 검색 결과를 Optional 객체로 반환
    // Optional 객체를 쓰는 이유: 널 체크 생략가능
}

MemberMapper는 회원과 관련된 데이터베이스 작업을 정의하는 인터페이스이다.
회원 저장, 조회 등의 기능을 선언하며 실제 SQL은 member-mapper.xml에서 작성한다.
MyBaitis는 MemberMapper와 XML을 연결하여 데이터베이스 작업을 수행한다.

PostMapper.java

package com.example.boardapp.mapper;

import java.util.List;
import java.util.Optional;

import org.apache.ibatis.annotations.Mapper;

import com.example.boardapp.domain.Post;

@Mapper // @Mapper: 이 인터페이스를 매퍼로 만들어라
public interface PostMapper { // 스프링이 내부적으로 이 인터페이스를 구현한다

    // 게시물 전체 가져오기
    List<Post> findAll();

    // id로 게시물 가져오기
    Optional<Post> findById(Long id);

    // 게시물 생성 처리
    void save(Post post);

    // 게시물 삭제 처리
    void delete(Long id);
}

PostMapper는 게시글(Post)과 관련된 데이터베이스 작업을 정의하는 인터페이스이다.
게시글 저장, 조회, 수정, 삭제 등의 기능을 선언하며 실제 SQL은 post-mapper.xml 파일에서 작성한다.
MyBatis는 PostMapper와 XML을 연결하여 게시글 데이터를 데이터베이스와 주고받을 수 있게 한다.

SecurityConfig.java

package com.example.boardapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration // 이 클래스를 설정(configuration) 클래스로 지정한다.
@EnableWebSecurity // 시큐리티용 설정파일로 지정한다.
public class SecurityConfig { // 시큐리티 설정 클래스. 시큐리티: 인증/인가(예를들어 로그인) 및 보안 담당

    // 비밀번호 암호화를 처리하는 객체(password encoder)를 빈(Bean)으로 등록하는 과정
    @Bean // Bean: 이 메서드(passwordEncoder)가 반환하는 객체를 빈으로 등록한다.
    public PasswordEncoder passwordEncoder() {
        // bcrypt: 암호화 알고리즘
        return new BCryptPasswordEncoder(); // BCryptPasswordEncoder: passwordEncoder(인터페이스)의 구현 클래스
    }

    // 필터 체인 (인증/인가 및 보안을 처리하는 필터들)
    @Bean // throws: 이 메서드에서 발생한 예외를 상위 메서드에게 전가
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        http
            // HTTP 요청(HTTP request)에 대해서 접근 권한을 부여하는 절차를 시작
            .authorizeHttpRequests(auth -> auth
                // /member/join(회원가입 페이지), /member/login(로그인 페이지)은 누구나 접근 가능
                // permit (허가한다)
                .requestMatchers("/member/join", "/member/login").permitAll()
                // 나머지 요청은 인증(로그인)이 필요하다
                .anyRequest().authenticated())
            // 폼 로그인(아이디/비밀번호 로그인)의 기본 설정 <> OAuth로그인, JWT 로그인
            .formLogin(form -> form
                // 로그인 페이지의 주소를 지정
                .loginPage("/member/login")
                // 로그인 처리 주소
                .loginProcessingUrl("/member/login")
                // 로그인에 성공한 경우 이동할 주소. 게시물 목록 페이지(/post)로 이동
                .defaultSuccessUrl("/post", true)
                .permitAll())
            // 로그아웃 설정
            .logout(logout -> logout
                // 로그아웃 요청 주소 (로그아웃 버튼을 누르면 이 주소를 요청하도록 만든다)
                .logoutUrl("/member/logout")
                // 로그아웃에 성공한 경우 이동시킬 페이지
                .logoutSuccessUrl("/member/login")
                // 로그아웃을 하면 세션을 무효화한다
                .invalidateHttpSession(true));
        
        return http.build();
    }
}

SecurityConfig.java

package com.example.boardapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration // 이 클래스를 설정(configuration) 클래스로 지정한다.
@EnableWebSecurity // 시큐리티용 설정파일로 지정한다.
public class SecurityConfig { // 시큐리티 설정 클래스. 시큐리티: 인증/인가(예를들어 로그인) 및 보안 담당

    // 비밀번호 암호화를 처리하는 객체(password encoder)를 빈(Bean)으로 등록하는 과정
    @Bean // Bean: 이 메서드(passwordEncoder)가 반환하는 객체를 빈으로 등록한다.
    public PasswordEncoder passwordEncoder() {
        // bcrypt: 암호화 알고리즘
        return new BCryptPasswordEncoder(); // BCryptPasswordEncoder: passwordEncoder(인터페이스)의 구현 클래스
    }

    // 필터 체인 (인증/인가 및 보안을 처리하는 필터들)
    @Bean // throws: 이 메서드에서 발생한 예외를 상위 메서드에게 전가
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        
        http
            // HTTP 요청(HTTP request)에 대해서 접근 권한을 부여하는 절차를 시작
            .authorizeHttpRequests(auth -> auth
                // /member/join(회원가입 페이지), /member/login(로그인 페이지)은 누구나 접근 가능
                // permit (허가한다)
                .requestMatchers("/member/join", "/member/login").permitAll()
                // 나머지 요청은 인증(로그인)이 필요하다
                .anyRequest().authenticated())
            // 폼 로그인(아이디/비밀번호 로그인)의 기본 설정 <> OAuth로그인, JWT 로그인
            .formLogin(form -> form
                // 로그인 페이지의 주소를 지정
                .loginPage("/member/login")
                // 로그인 처리 주소
                .loginProcessingUrl("/member/login")
                // 로그인에 성공한 경우 이동할 주소. 게시물 목록 페이지(/post)로 이동
                .defaultSuccessUrl("/post", true)
                .permitAll())
            // 로그아웃 설정
            .logout(logout -> logout
                // 로그아웃 요청 주소 (로그아웃 버튼을 누르면 이 주소를 요청하도록 만든다)
                .logoutUrl("/member/logout")
                // 로그아웃에 성공한 경우 이동시킬 페이지
                .logoutSuccessUrl("/member/login")
                // 로그아웃을 하면 세션을 무효화한다
                .invalidateHttpSession(true));
        
        return http.build();
    }
}

SecurityConfig 클래스는 Spring Security의 보안 설정을 담당하는 클래스이다.
비밀번호 암호화 방식(BYcrypt)을 설정하고 로그인 및 로그아웃 기능, 페이지 접근 권한 등의 보안 규칙을 정의한다.
이를 통해 인증(Authentication)과 인가(Authorization)을 처리하여 애플리케이션의 보안을 관리할 수 있다.

CustomUserDetailsService.java

package com.example.boardapp.config;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

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

// 데이터베이스로부터 인증을 요청한 사용자(로그인을 요청한 사용자)의 정보를 가져오는 서비스
@Service // 서비스 빈으로 등록. 컴포넌트 스캔 방식: 빈으로 등록하고자 하는 클래스에 어노테이션을 붙인다
public class CustomUserDetailsService implements UserDetailsService{

    // 멤버 매퍼객체에 의존한다. 데이터베이스와 통신하는 부분이기 때문에
    private final MemberMapper memberMapper;
    // final: 이 필드를 불변성을 가지도록 만든다

    public CustomUserDetailsService(MemberMapper memberMapper) {
        // final로 선언한 멤버에게 값할당을 안하면 컴파일 에러가 발생
        this.memberMapper = memberMapper;
    }

    // 데이터베이스로부터 입력받는 사용자이름을 가진 유저를 조회
    // UserDetails(반환타입): 시큐리티가 인증에 사용하는 객체 규격
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 데이터베이스로부터 사용자 이름에 해당하는 멤버를 검색
        Member member = memberMapper.findByUsername(username)

        // Optional의 메서드. 검색 결과가 없으면 예외를 던진다 (널 체크할 필요x)
        .orElseThrow(() ->
        new UsernameNotFoundException("존재하지 않는 사용자입니다"));
        
        // UserDetails 객체 생성 후 반환
        return User.builder() // 빌더 패턴으로 객체(UserDetails)를 생성
            .username(member.getUsername())
            .password(member.getPassword())
            .build();
    }
}

CustomUserDetailsService 클래스는 Spring Security의 UserDetailsService를 구현한 클래스이다.
사용자가 로그인할 때 입력한 아이디를 기반으로 데이터 베이스에서 회원 정보를 조회하고 Spring Security가 사용할 UserDetails 객체를 생성하여 반환한다.
회원 정보가 존재하지 않을 경우 예외를 발생시켜 로그인 실패를 처리한다.


마무리

이번에는 게시판 프로젝트를 구현하기 위해 MyBatis 매퍼와 도메인, DTO, Spring Security 관련 클래스들을 작성하였다. 특히 post-mapper.xml을 통해 게시글과 회원 정보를 조회하는 SQL문을 작성하고, resultMap과 association을 활용하여 데이터베이스의 데이터를 객체와 연결하는 과정을 학습하였다. 또한 게시글 전체 조회, 게시글 상세 조회, 게시글 저장, 삭제 기능을 위한 매퍼를 구현하며 데이터베이스와 자바 객체가 어떻게 연결되는지 이해할 수 있었다.

Member 클래스와 Post 클래스를 작성하면서 데이터베이스의 테이블을 자바 객체로 표현하는 방법을 배웠다. 특히 Post 객체 안에 Member 객체를 포함하여 게시글과 작성자의 연관관계를 표현하는 구조가 인상적이었다. 또한 SignUpRequest, PostListResponse, PostDetailsResponse와 같은 DTO를 작성하며 화면에 필요한 데이터만 전달하는 역할을 수행한다는 점을 알게 되었다.

MemberMapper와 PostMapper를 작성하면서 데이터베이스 작업을 인터페이스로 정의하고, 실제 SQL은 XML 파일에서 작성한다는 MyBatis의 구조를 배웠다. Optional을 사용하여 조회 결과가 없을 경우를 처리하는 방법도 함께 학습할 수 있었다.

추가로 SecurityConfig와 CustomUserDetailsService를 작성하며 Spring Security를 활용한 인증 기능을 구현하였다. BCryptPasswordEncoder를 사용하여 비밀번호를 암호화하고, 로그인과 로그아웃, 페이지 접근 권한을 설정하는 방법을 배웠다. 또한 사용자가 로그인할 때 데이터베이스에서 회원 정보를 조회하여 UserDetails 객체로 변환하는 과정도 확인할 수 있었다.

이번 내용은 데이터베이스, 객체 매핑, DTO, 인증과 인가 등 다양한 개념이 함께 등장하여 이전보다 훨씬 어렵게 느껴졌다. 특히 resultMap, association, UserDetailsService, SecurityFilterChain과 같은 개념은 처음 접했을 때 이해하기 쉽지 않았다. 하지만 게시글과 회원 정보가 데이터베이스에서 조회되어 객체로 변환되고, 로그인 기능과 연결되는 전체 흐름을 확인하면서 웹 애플리케이션이 동작하는 과정을 조금씩 이해할 수 있었다. 앞으로는 각 클래스의 역할과 데이터 흐름을 반복해서 정리하며 익숙해질 필요가 있다고 느꼈다.

0개의 댓글