๐Ÿ’ท์‹œํ๋ฆฌํ‹ฐ ๋กœ๊ทธ์ธ ์ธ์ฆ ๋ฐ ๊ถŒํ•œ, ์ƒํ’ˆ ๋“ฑ๋กํŽ˜์ด์ง€ ๊ทธ๋ฆฌ๊ณ  ๐Ÿ”ฅ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๋งคํ•‘๐Ÿ”ฅ

gdhiยท2023๋…„ 12์›” 7์ผ
post-thumbnail

๐Ÿ’ท์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋กœ ๋กœ๊ทธ์ธ ํ•˜๊ธฐ

์ „ ์‹œ๊ฐ„ ๊นŒ์ง€ ์™„์ „ ๊ฐ„๋‹จํ•œ ํšŒ์› ๊ฐ€์ž…๋งŒ ํ•ด๋ณด์•˜๋‹ค. ์ด๋ฒˆ์—” ๋กœ๊ทธ์ธ์„ ํ•ด๋ณด์ž


๐Ÿ“ŒMemberService ์ƒ์† ๋ฐ›๊ธฐ

package com.shop.service;

import com.shop.entity.Member;
import com.shop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
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 org.springframework.transaction.annotation.Transactional;






// ์„œ๋น„์Šค
@Service
// ํŠธ๋žœ์žญ์…˜ ์„ค์ • : ์„ฑ๊ณตํ•˜๋ฉด ์ ์šฉ ์‹คํŒจํ•˜๋ฉด ๋กค๋ฐฑ
@Transactional
// final ๋˜๋Š” @NonNull ๋ช…๋ น์–ด๊ฐ€ ๋ถ™์œผ๋ฉด ๊ฐ์ฒด๋ฅผ ์ž๋™์œผ๋กœ ๋ถ™์—ฌ์ค€๋‹ค. @Autowired๊ฐ€ ํ•„์š” ์—†๋‹ค๋Š” ๋œป
@RequiredArgsConstructor // ๋กฌ๋ณต ์–ด๋…ธํ…Œ์ด์…˜
public class MemberService implements UserDetailsService { // implements? ๐Ÿ‘‰ UserDetailsService๋Š” ์ธํ„ฐํŽ˜์ด์Šค ๐Ÿ‘‰ ์ถ”์ƒํ™” ๋ฉ”์†Œ๋“œ ์žฌ์ •์˜

    //@Autowired
    //MemberRepository memberRepository;
    private final MemberRepository memberRepository;

    // ์ค‘๋ณต ๊ฒ€์‚ฌ ํ›„์— ์—†์œผ๋ฉด ์ €์žฅ
    public Member saveMember(Member member) {
        validateDuplicateMember(member);
        return memberRepository.save(member); // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ์„ ํ•˜๋ผ๋Š” ๋ช…๋ น
    }

    // ์ด๋ฉ”์ผ ์ค‘๋ณต ๊ฒ€์‚ฌ ๋ฉ”์†Œ๋“œ
    private void validateDuplicateMember(Member member) {
        Member findMember = memberRepository.findByEmail(member.getEmail());

        // Controller ์—์„œ try/catch ๋กœ ๋‚˜์˜ค๊ฒŒ
        if (findMember != null) {
            throw new IllegalStateException("์ด๋ฏธ ๊ฐ€์ž…๋œ ํšŒ์›์ž…๋‹ˆ๋‹ค.");
        }
    }


    // UserDetailsService : ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํšŒ์› ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์—ญํ•  ์ธํ„ฐํŽ˜์ด์Šค
    // UserDetail : ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ํšŒ์› ์ •๋ณด๋ฅผ ๋‹ด๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค
    // UserDetails๋กœ usernameParameter("email")๋ฅผ ๊ฐ€์ง€๊ณ  ๋กœ๊ทธ์ธ ์ธ์ฆ. 
    // ์ฆ‰, ์ง์ ‘ ๊ตฌํ˜„ํ•˜๊ฑฐ๋‚˜ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ์ œ๊ณตํ•˜๋Š” User ํด๋ž˜์Šค ์‚ฌ์šฉ (๊ตฌํ˜„์ฒด)
    // loadUserByUsername() ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ํšŒ์›์ •๋ณด๋ฅผ ์กฐํšŒ -> UserDetails ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ˜ํ™˜
    @Override
    public UserDetails loadUserByUsername(String email){
        Member member = memberRepository.findByEmail(email);

        if (member == null){
            throw new UsernameNotFoundException(email);
        }

        //๋นŒ๋” ํŒจํ„ด
        return User.builder().username(member.getEmail())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();
    }

}



๐Ÿ“ŒSecurityConfig ํด๋ž˜์Šค ์ˆ˜์ •

package com.shop.config;

import com.shop.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    MemberService memberService;

    // ์ „๋ถ€ ํ—ˆ๋ฝ
    // filterChain(HttpSecurity http) ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ์ธ ๋ฐ ๋กœ๊ทธ์•„์›ƒ URL ์ง€์ •
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.authorizeRequests(auth -> auth.requestMatchers("/", "/members/**").permitAll())
                // http.formLogin() - http ๋ฅผ ํ†ตํ•ด ๋“ค์–ด์˜ค๋Š” form ๊ธฐ๋ฐ˜ request ๋ฅผ ์ด์šฉํ•˜์—ฌ Login ์„ ์ฒ˜๋ฆฌ
				// form ํƒœ๊ทธ์—์„œ ์‚ฌ์šฉ์ž์˜ ID ๋ถ€๋ถ„์€ default ๊ฐ’์œผ๋กœ "username" ํ•„๋“œ
                .formLogin(formLogin -> formLogin
                        .loginPage("/members/login") // ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€
                        .defaultSuccessUrl("/") // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํŽ˜์ด์ง€ ๐Ÿ‘‰ "/"
                        .usernameParameter("email")
                        .failureUrl("/members/login/error")) // ๋กœ๊ทธ์ธ ์—๋Ÿฌ ํŽ˜์ด์ง€
                // ๋กœ๊ทธ์•„์›ƒ
                .logout(logout -> logout
                        .logoutRequestMatcher(new AntPathRequestMatcher("/members/logout")) // ๋กœ๊ทธ์•„์›ƒ ํŽ˜์ด์ง€
                        .logoutSuccessUrl("/")); // ๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต ํŽ˜์ด์ง€ ๐Ÿ‘‰ "/"

        return  http.build();

    }

    // ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /*
    AuthenticationManagerBuilder ๋ฅผ ํ†ตํ•ด AuthenticationManager ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์ธ์ฆ ์ฒ˜๋ฆฌ ์ˆ˜ํ–‰
	UserDetailsService ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  loadUserByUsername ๋ฉ”์†Œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•œ 
    memberService ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•˜์—ฌ User ๊ฐ์ฒด๋ฅผ ์–ป์–ด๋‚ธ ๋’ค, 
    ์ง€์ •๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ๋ฐฉ์‹์œผ๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜๋Š”์ง€ ๊ฒ€์ฆ
    */
    @Autowired
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(memberService).passwordEncoder(passwordEncoder());
    }


}



๐Ÿ“ŒmemberLoginFrom.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">
<!-- ์‚ฌ์šฉ์ž CSS ์ถ”๊ฐ€ -->
<th:block layout:fragment = "css">
    <style>
        .error{
            color: red;
        }
    </style>
</th:block>

<div layout:fragment = "content">
    <form action="/members/login" role="form" method="post">
        <div class = "form-group">
            <label th:for = "email">์ด๋ฉ”์ผ</label>
            <input type="email" name="email" class="form-control" placeholder="์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”">
        </div>
        <div class = "form-group">
            <label th:for = "password">๋น„๋ฐ€๋ฒˆํ˜ธ</label>
            <input type="password" name="password" class="form-control" placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”">
        </div>

        <p th:if="${loginErrorMsg}" class="error" th:text="${loginErrorMsg}"></p>

        <br>

        <button class="btn btn-success">๋กœ๊ทธ์ธ</button>

        <button type="button" class="btn btn-warning" onclick="location.href='/members/new'">ํšŒ์›๊ฐ€์ž…</button>

        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

    </form>
</div>

</html>



๐Ÿ“ŒMemberController ํด๋ž˜์Šค ์ˆ˜์ •

package com.shop.controller;

import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import com.shop.service.MemberService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.Value;
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.*;


@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    private final PasswordEncoder passwordEncoder;

    // method = "get" ์ผ ๋•Œ ์‹คํ–‰
    @GetMapping(value = "/new")
    public String memberForm(Model model){

        model.addAttribute("memberFormDto", new MemberFormDto());

        return "member/memberForm";
    }

    // method = "post" ์ผ ๋•Œ ์‹คํ–‰ DB์— ์ €์žฅ
    @PostMapping(value = "/new")
    // memberForm ์˜ค๋ฒ„๋กœ๋”ฉ
    public String memberForm(@Valid MemberFormDto memberFormDto, BindingResult bindingResult, Model model){

        // ์—๋Ÿฌ๋‚˜๋ฉด ๋‹ค์‹œ ๊ฐ€์ž…
        if(bindingResult.hasErrors()){

            return "member/memberForm";

        }

        try {

            Member member = Member.createMember(memberFormDto, passwordEncoder);

            memberService.saveMember(member);

        }catch (IllegalStateException e){

            model.addAttribute("errorMessage", e.getMessage());

            return "member/memberForm";

        }

        return "redirect:/";

    }
    
    
    // Header์—์„œ ๋ˆŒ๋ €์„ ๋•Œ ์‹คํ–‰ ๋˜์–ด "/member/memberLoginForm"๋กœ ๋ณด๋‚ธ๋‹ค. ๊ทธ๋ž˜์„œ GetMapping
    // memberLoginForm ์—์„œ์˜ post๋Š” SecurityConfig  .loginPage("/members/login") ์œผ๋กœ ๊ฐ„๋‹ค
    // ๋ณต์žก....
    @GetMapping(value = "/login")
    public String loginMember(){

        return "/member/memberLoginForm";

    }

    // ๋กœ๊ทธ์ธ์ด ์‹คํŒจ ํ–ˆ์„ ๋•Œ ์‹คํ–‰
    @GetMapping(value = "login/error")
    public String loginError(Model model){

        model.addAttribute("loginErrorMsg", "์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”");

        return "/member/memberLoginForm";
    }


}

๐Ÿ‘‰ ์ถ”๊ฐ€



๐Ÿ“Œ๊ฒฐ๊ณผ

์‹คํ–‰ ํ•ด๋ณด๋ฉด
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'securityConfig': Requested bean is currently in creation: Is there an unresolvable circular reference?
์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ ํ•  ํ…๋ฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ”„๋กœํผํ‹ฐ์— ์ˆœํ™˜์„ ์ถ”๊ฐ€ ํ•ด์ค˜์•ผ ํ•œ๋‹ค. ์•„๋งˆ ๋ฒ„์ „์ด ๋ฐ”๋€Œ๋ฉด์„œ์„œ ๋ณ€๊ฒฝ ๋œ ๊ฒƒ์œผ๋กœ ์ถ”์ •

    // ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”
    @Bean
    public static PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

๐Ÿ‘‰ SecurityConfig ์—์„œ ์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝ ํ•ด์ค˜์•ผ ํ•œ๋‹ค

๐Ÿ‘‰ ๋กœ๊ทธ์ธ ์‹คํŒจ

๐Ÿ‘‰ ์„ฑ๊ณต, ๋กœ๊ทธ์•„์›ƒ ๊นŒ์ง€ ์ž˜ ๋œ๋‹ค



โ“์–ด๋–ค ๊ตฌ์กฐ๋กœ ์‹คํ–‰?

1. memberLoginForm.html ๋กœ๊ทธ์ธ


๐Ÿ‘‰ ํด๋ฆญ

์— ์˜ํ•ด
\

2. ๋กœ๊ทธ์ธ์„ ๋ˆŒ๋ €์„ ๊ฒฝ์šฐ



๐Ÿ‘‰ ์‹คํ–‰ ํ›„

๐Ÿ‘‰ ์ธ์ฆ ์‹คํ–‰

๐Ÿ‘‰ .usernameParameter("email") ๋กœ email ์„ ๊ฐ€์ง€๊ณ  select ์‹คํ–‰

๐Ÿ‘‰ member email ๊ฐ์ฒด๋ฅผ ํ•˜๋‚˜(์ค‘๋ณต x) ๋ฐ›๋Š”๋‹ค. null ์ด๋ฉด ์—๋Ÿฌ throw

๐Ÿ‘‰ null์ด ์•„๋‹ˆ๋ฉด View์—์„œ ๋‚ด๋ ค์˜จ email๊ณผ password๋ฅผ UserDetails์™€ ๋น„๊ตํ•ด์„œ ํ™•์ธ. password๋Š” ์ด๋ฏธ encoder๋ฅผ ํ•ด๋†“์Œ.
๐Ÿ‘‰ ์ผ์น˜ํ•œ๋‹ค? ๋กœ๊ทธ์ธ ์„ฑ๊ณต

3. ๋กœ๊ทธ์ธ ์‹คํŒจ ํ–ˆ์„ ๋•Œ




๐Ÿ‘‰ ์‹คํ–‰









๐Ÿ“–์‹œํ๋ฆฌํ‹ฐ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

JUnit ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š” ์Šต๊ด€์„ ๋“ค์—ฌ์•ผ ํ•œ๋‹ค

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

๐Ÿ‘‰ ์˜์กด์„ฑ ์ถ”๊ฐ€


๐Ÿ“ŒMemberController test ์ƒ์„ฑ


๐Ÿคฆโ€โ™€๏ธ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ

package com.shop.controller;

import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import com.shop.service.MemberService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;

@SpringBootTest
// ๊ฐ€์ƒ์˜ ์›น์„ ๋งŒ๋“ค์–ด ์ฃผ๋Š” mockMvc
@AutoConfigureMockMvc
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberControllerTest {

    @Autowired
    private MemberService memberService;

    // mockMvc : ์›น ๋ธŒ๋ผ์šฐ์ €์— ์š”์ฒญ์„ ํ•˜๋Š” ๊ฒƒ ์ฒ˜๋Ÿผ ํ…Œ์Šค๋ฅผ๋ฅผ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ฐ€์งœ ๊ฐ์ฒด
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    PasswordEncoder passwordEncoder;

     /*
     1. memberFormDto ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด์„œ ๋”๋ฏธ๋ฐ์ดํ„ฐ์— ๋„ฃ๋Š”๋‹ค.
     2. Member ๐Ÿ‘‰ createMember๋ฅผ ํ˜ธ์ถœํ•˜๋Š”๋ฐ
     ๋งค๊ฐœ๋ณ€์ˆ˜ memberFormDto, passwordEncoder๋กœ Member ๊ฐ์ฒด ์ƒ์„ฑ
     3. memberService.saveMember ๐Ÿ‘‰ Member๋ฅผ ํ…Œ์ด๋ธ”์— ์ €์žฅ
     validateDuplicateMember๋ฅผ ์ด์šฉํ•ด ๊ธฐ์กด ๊ฐ€์ž… ์—ฌ๋ถ€ ํ™•์ธ
     ๊ฐ€์ž…์ด ์•ˆ๋œ ๊ฒฝ์šฐ : memberRepository.save(member) ๋กœ ํ…Œ์ด๋ธ”์— ์ €์žฅ
     ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ์ด ๋˜๋ฉด ๋˜‘๊ฐ™์€ Member ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜
     ๊ฐ€์ž…์ด ๋œ ๊ฒฝ์šฐ : IllegalStateException("์ด๋ฏธ ๊ฐ€์ž…๋œ ํšŒ์›์ž…๋‹ˆ๋‹ค.")
     */
    public Member createMember(String email, String password){

        MemberFormDto memberFormDto = new MemberFormDto();

        memberFormDto.setEmail(email);
        memberFormDto.setPassword(password);
        memberFormDto.setName("ํ™๊ธธ๋™");
        memberFormDto.setAddress("์„œ์šธ์‹œ ๋งˆํฌ๊ตฌ ํ•ฉ์ •๋™");
        memberFormDto.setTelNumber("010-1234-5678");

        Member member = Member.createMember(memberFormDto, passwordEncoder);

        return memberService.saveMember(member);

    }

    @Test
    @DisplayName("๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ")
    public void loginSuccessTest() throws Exception {
        String email = "test@email.com";
        String password = "1234";
        this.createMember(email, password); // ์œ„ ๋ณ€์ˆ˜๋ฅผ ๋„ฃ์–ด createMember๋กœ DB์— ๋ฐ์ดํ„ฐ ์ €์žฅ

        // ๊ฐ€์ƒ ์›น ์‹คํ–‰ ๐Ÿ‘‰ ๋กœ๊ทธ์ธ ์‹คํ–‰ userParameter("email")๋ฅผ ๊ฐ€์ง€๊ณ 
        // "/members/login" ์—์„œ user : "test@email.com", password : "1234" ๋ฅผ
        // mockMvc ๊ฐ€์ƒ ์›น์—์„œ authenticated()๋กœ ์ธ์ฆ๋œ ๊ฑธ๋กœ ๊ธฐ๋Œ€ ํ•œ๋‹ค. ์•ˆ๋˜๋ฉด ํ…Œ์ŠคํŠธ ์‹คํŒจ
        mockMvc.perform(formLogin().userParameter("email")
                .loginProcessingUrl("/members/login")
                .user(email)
                .password(password))
                .andExpect(SecurityMockMvcResultMatchers.authenticated());



    }

}



๐Ÿคฆโ€โ™€๏ธ๋กœ๊ทธ์ธ ์‹คํŒจ ํ…Œ์ŠคํŠธ

    @Test
    @DisplayName("๋กœ๊ทธ์ธ ์‹คํŒจ ํ…Œ์ŠคํŠธ")
    public void loginFailedTest() throws Exception {
        String email = "test@email.com";
        String password = "1234";
        this.createMember(email, password);

        mockMvc.perform(formLogin().userParameter("email")
                        .loginProcessingUrl("/members/login")
                        .user(email)
                        .password("12345"))
                 // ๋กœ๊ทธ์ธ ์ธ์ฆ์ด ๋˜์ง€ ์•Š๊ธฐ๋ฅผ ๊ธฐ๋Œ€ ๐Ÿ‘‰ ๋กœ๊ทธ์ธ์ด ์‹คํŒจํ•˜๊ธฐ๋ฅผ ์›ํ•œ๋‹ค
                .andExpect(SecurityMockMvcResultMatchers.unauthenticated());
    }









๐Ÿ’ท์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋„ฃ๊ธฐ

Thymeleaf Extras Springsecurity6

        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity6</artifactId>
            <version>3.1.2.RELEASE</version>
        </dependency>

๐Ÿ‘‰ ์˜์กด์„ฑ ์ถ”๊ฐ€


๐Ÿ“Œheader.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security" lang="ko"> ๐Ÿ‘‰ sec ์ถ”๊ฐ€
<head>

...

<ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <!-- ADMIN๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ hasAnyAuthority('ROLE_ADMIN')-->
                    <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
                        <a class="nav-link" href="/admin/item/new">์ƒํ’ˆ ๋“ฑ๋ก</a>
                    </li>
                    <li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
                        <a class="nav-link" href="/admin/items">์ƒํ’ˆ ๊ด€๋ฆฌ</a>
                    </li>
                    <!-- ๋กœ๊ทธ์ธ์ด ๋œ ๊ฒฝ์šฐ isAuthenticated -->
                    <li class="nav-item" sec:authorize="isAuthenticated()">
                        <a class="nav-link" href="/cart">์žฅ๋ฐ”๊ตฌ๋‹ˆ</a>
                    </li>
                    <li class="nav-item" sec:authorize="isAuthenticated()">
                        <a class="nav-link" href="/orders">๊ตฌ๋งค์ด๋ ฅ</a>
                    </li>
                    <!-- ์•„๋ฌด๋‚˜ ์ ‘๊ทผ ๊ฐ€๋Šฅ isAnonymous-->
                    <li class="nav-item" sec:authorize="isAnonymous()">
                        <a class="nav-link" href="/members/login">๋กœ๊ทธ์ธ</a>
                    </li>
                    <li class="nav-item" sec:authorize="isAuthenticated()">
                        <a class="nav-link" href="/members/logout">๋กœ๊ทธ์•„์›ƒ</a>
                    </li>
                </ul>

...



๐Ÿ“Œ๊ฒฐ๊ณผ

๐Ÿ‘‰ ๋กœ๊ทธ์ธ๋งŒ ํ‘œ์‹œ

๐Ÿ‘‰ ๋ณ€๊ฒฝํ›„์— Apply

๐Ÿ‘‰ USER ๋กœ๊ทธ์ธ ์„ฑ๊ณต

๐Ÿ‘‰ ADMIN ๋กœ๊ทธ์ธ ์„ฑ๊ณต









๐Ÿ’ท์ƒํ’ˆํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ


๐Ÿ“ŒitemForm.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
      layout:decorate="~{layouts/layout1}" lang="ko">

<!-- ์น˜ํ™˜ ๋˜๋Š” ๋…€์„์ด๋‹ˆ๊นŒ head, body๋Š” ํ•„์š” ์—†๋‹ค -->
<div layout:fragment="content">
    <h1>์ƒํ’ˆ ๋“ฑ๋ก ํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค.</h1>
</div>

</html>



๐Ÿ“ŒItemController ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ItemController {

    @GetMapping(value = "/admin/item/new") // ADMIN ROLE ๋งŒ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๋‹ค
    public String itemForm(){
        return "/item/itemForm";
    }
    
}



๐Ÿ“ŒCustomAuthenticationEntryPoint ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.config;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import java.io.IOException;

// AuthenticationEntryPoint ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ ํด๋ž˜์Šค
// ์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๊ฐ€ ๋ฆฌ์†Œ์Šค ์š”์ฒญ ์‹œ "Unauthorized" ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ด
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authenticationException) throws IOException, ServletException {
        // int SC_UNAUTHORIZED = 401; ๐Ÿ‘‰ ์šฐ๋ฆฌ๊ฐ€ ์ธ์ฆ ์—๋Ÿฌ(401 ์—๋Ÿฌ)๋ฅผ, "Unauthorized"๋กœ  ๋ฐ”๊พผ ๊ฒƒ
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
        
    }

}



๐Ÿ“ŒSecurityConfig ํด๋ž˜์Šค ์ˆ˜์ •


...

        http.authorizeRequests(auth -> auth.requestMatchers("/", "/members/**", "/item/**" , "/images/**").permitAll() // ๋ˆ„๊ตฌ๋‚˜ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ํŽ˜์ด์ง€
                        .requestMatchers("/css/**", "/js/**", "/img/**").permitAll() // static ํด๋” ์•ˆ์— ์žˆ๋Š” /css, /js, /img ํ•˜์œ„ ๋ชจ๋“  ํŒŒ์ผ์€ ์ธ์ฆ ๋ฌด์‹œ
                        .requestMatchers("/admin/**").hasRole("ADMIN") // ADMIN๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ํŽ˜์ด์ง€
                        .anyRequest().authenticated()) // ์œ„์— ์กด์žฌํ•˜๋Š” url patterns ๋“ค์„ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€ ์š”์ฒญ๋“ค

...

									                        .logoutSuccessUrl("/")); // ๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต ํŽ˜์ด์ง€ ๐Ÿ‘‰ "/"

        // ๊ถŒํ•œ์— ๋งž์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•  ๋•Œ ์ˆ˜ํ–‰๋˜๋Š” ํ•ธ๋“ค๋Ÿฌ
        // ๊ถŒํ•œ์ด ์—†๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋ฆฌ์†Œ์Šค๋ฅผ ์š”์ฒญํ•˜๋ฉด "Unauthorized" ์—๋Ÿฌ ๋ฐœ์ƒ
        http.exceptionHandling(exceptHand -> exceptHand
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint()));
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint()));


        return  http.build();
        
        ...
        
        



๐Ÿ“Œ๊ฒฐ๊ณผ

๐Ÿ‘‰ ํšŒ์›๊ฐ€์ž…

๐Ÿ‘‰ http://localhost/admin/item/new ์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋‹ค

๐Ÿ‘‰ member.setRole(Role.USER); ๋กœ ๋ฐ”๊พธ๊ณ 

๐Ÿ‘‰ USER๋Š” item/new์— ์ ‘๊ทผ์ด ๋ถˆ๊ฐ€ํ•˜๋‹ค. requestMatchers("/admin/**").hasRole("ADMIN") ์„ค์ • ํ–ˆ๊ธฐ ๋•Œ๋ฌธ



๐Ÿคฆโ€โ™€๏ธItemController ํ…Œ์ŠคํŠธํ•˜๊ธฐ

package com.shop.controller;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@SpringBootTest
// ๊ฐ€์ƒ์˜ ์›น์„ ๋งŒ๋“ค์–ด ์ฃผ๋Š” mockMvc
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
class ItemControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    @DisplayName("์ƒํ’ˆ ๋“ฑ๋ก ํŽ˜์ด์ง€ ๊ถŒํ•œ ํ…Œ์ŠคํŠธ")
    // ํšŒ์›์˜ ์ด๋ฆ„์ด admin์ด๊ณ  role(๊ถŒํ•œ)์ด ADMIN์ธ ์œ ์ €๊ฐ€ ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๊ฐ€์ƒ์˜ UserDetails ๊ฐ์ฒด ๊ตฌํ˜„
    @WithMockUser(username = "admin", roles = "ADMIN")
    public void itemFormTest() throws Exception {
    	//๊ฐ€์ƒ์˜ Get ๋ฐฉ์‹ URL Request ์ƒ์„ฑ
        // ์š”์ฒญ ๋นŒ๋”๋ฅผ get์œผ๋กœ ๋งŒ๋“ฆ "/admin/item/new" ์—์„œ admin์— ๊ฑธ๋ฆฌ๊ฒŒ ๋œ๋‹ค.
        mockMvc.perform(MockMvcRequestBuilders.get("/admin/item/new")) // get ์š”์ฒญ
                .andDo(print()) // ์ฝ˜์†”์ฐฝ์— ์ถœ๋ ฅ
                .andExpect(status().isOk()); // ์‘๋‹ต ์ƒํƒœ๊ฐ€ OK์ธ์ง€ ์ฒดํฌ
    }

    @Test
    @DisplayName("์ƒํ’ˆ ๋“ฑ๋ก ํŽ˜์ด์ง€ ์ผ๋ฐ˜ ํšŒ์› ์ ‘๊ทผ ํ…Œ์ŠคํŠธ")
    @WithMockUser(username = "user", roles = "USER") // ๊ฐ€์ƒ์˜ role๊ณผ username ์„ค์ •
    public void itemFormNotAdminTest() throws Exception {
        // ์š”์ฒญ ๋นŒ๋”๋ฅผ get์œผ๋กœ ๋งŒ๋“ฆ "/admin/item/new" ์—์„œ admin์— ๊ฑธ๋ฆฌ๊ฒŒ ๋œ๋‹ค.
        mockMvc.perform(MockMvcRequestBuilders.get("/admin/item/new")) // get ์š”์ฒญ
                .andDo(print()) // Request & Response ๋ฉ”์‹œ์ง€ ์ฝ˜์†”์ฐฝ์— ์ถœ๋ ฅ
                .andExpect(status().isForbidden()); // ์‘๋‹ต์ด Forbidden์ด ๋ฐœ์ƒ ๐Ÿ‘‰ ๊ถŒํ•œ ์—†๋Š” ์‚ฌ์šฉ์ž ์ ‘๊ทผ 403 ์—๋Ÿฌ
    }

}

๐Ÿ“๊ฒฐ๊ณผ

๐Ÿ‘‰ ADMIN, ์—ฌ๋Ÿฌ ์ •๋ณด๊ฐ€ ๋‹ด๊ฒจ์žˆ๋‹ค

๐Ÿ‘‰ USER Status = 403, Error message = Forbidden ์—๋Ÿฌ









๐Ÿ“–๐Ÿ”ฅโญ(Spring 1๋Œ€์žฅ)Entity ์—ฐ๊ด€ ๊ด€๊ณ„ ๋งคํ•‘โญ๐Ÿ”ฅ

์—ฐ๊ด€ ๊ด€๊ณ„ : ํ…Œ์ด๋ธ” ์™ธ๋ž˜ํ‚ค(FK) ์—ฐ๊ฒฐ ๐Ÿ‘‰ ์ƒ๋Œ€๋ฐฉ์˜ ๊ธฐ๋ณธํ‚ค(PK)
๋ณดํ†ต์€ 1:N ์ด๋ฉด N์ธ ๋…€์„์ด ์™ธ๋ž˜ํ‚ค๋ฅผ ๊ฐ–๊ณ  1์€ ๊ธฐ๋ณธํ‚ค๋ฅผ ์ฃผ๋Š” ์—ญํ• . ์•„๋‹Œ ๊ฒฝ์šฐ๋„ ์žˆ๋‹ค

  1. 1:1 ๊ด€๊ณ„ : @OnetoOne, ํšŒ์›๊ฐ€์ž…๊ณผ ํšŒ์›/์œ ์ €์™€ ์žฅ๋ฐ”๊ตฌ๋‹ˆ
  2. 1:N ๊ด€๊ณ„ : @OnetoMany, ๊ต์ˆ˜์™€ ํ•™์ƒ/์žฅ๋ฐ”๊ตฌ๋‹ˆ์™€ ์ƒํ’ˆ
  3. N:1 ๊ด€๊ณ„ : @ManytoOne, ์œ„์™€ ๋ฐ˜๋Œ€
  4. N:M ๊ด€๊ณ„ : @ManytoMany, ์œ ์ €๋“ค๊ณผ ์ƒํ’ˆ

    ๋‹จ๋ฐฉํ–ฅ/์–‘๋ฐฉํ–ฅ

  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ค‘์‹ฌ ์„ค๊ณ„ ํ…Œ์ด๋ธ”์—์„œ ๊ด€๊ณ„๋Š” ํ•ญ์ƒ ์–‘๋ฐฉํ–ฅ
  • JPA ๊ฐ์ฒด์ง€ํ–ฅ ์ค‘์‹ฌ ์„ค๊ณ„์—์„œ๋Š” ๋‹จ๋ฐฉํ–ฅ, ์–‘๋ฐฉํ–ฅ ์กด์žฌ



๐Ÿ“Œ1:1 ๋‹จ๋ฐฉํ–ฅ ๊ด€๊ณ„

์žฅ๋ฐ”๊ตฌ๋‹ˆ(cart) โŸถ ์œ ์ €(member), ์žฅ๋ฐ”๊ตฌ๋‹ˆ๊ฐ€ ์œ ์ € PK๋ฅผ ๊ฐ–๊ณ  ์žˆ๋Š” ์ƒํ™ฉ


๐Ÿ“Cart Entity ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.entity;

import jakarta.persistence.*;
import lombok.Data;

@Entity
@Table(name = "cart")
@Data
public class Cart {

    @Id
    @Column(name = "cart_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @OneToOne // 1:1 ๋งคํ•‘
    @JoinColumn(name = "member_id") // JoinColumn ๋งคํ•‘ ํ•  ์™ธ๋ž˜ํ‚ค ์ง€์ •. member_id ์ด๋ฆ„ ์„ค์ •
    //name์„ ๋ช…์‹œํ•˜์ง€ ์•Š์œผ๋ฉด JPA๊ฐ€ ์•Œ์•„์„œ ID๋ฅผ ์ฐพ๊ธดํ•˜์ง€๋งŒ ์›ํ•˜๋Š” name์ด ์•„๋‹ ์ˆ˜๋„ ์žˆ๋‹ค
    private Member member;

}



๐Ÿ“๊ฒฐ๊ณผ

Hibernate: 
    create table cart (
        cart_id bigint not null,
        member_id bigint,
        primary key (cart_id)
    ) engine=InnoDB
    
    ...
    
Hibernate: 
    create table item (
        price integer not null,
        stock_number integer not null,
        item_id bigint not null,
        reg_time datetime(6),
        update_time datetime(6),
        item_nm varchar(50) not null,
        item_detail tinytext not null,
        item_sell_status enum ('SELL','SOLD_OUT'),
        primary key (item_id)
    ) engine=InnoDB   
    
    ...
    
Hibernate: 
    create table member (
        member_id bigint not null,
        address varchar(255),
        email varchar(255),
        name varchar(255),
        password varchar(255),
        tel_number varchar(255),
        role enum ('USER','ADMIN'),
        primary key (member_id)
    ) engine=InnoDB
    
    ...
    
Hibernate: 
    alter table cart 
       add constraint UK_7dds3r67nkhxm9sbs9r5obd46 unique (member_id)
Hibernate: 
    alter table member 
       add constraint UK_mbmcqelty0fbrvxp1q58dn57t unique (email)
Hibernate: 
    alter table cart 
       add constraint FKix170nytunweovf2v9137mx2o 
       foreign key (member_id) 
       references member (member_id)

 

๐Ÿ‘‰ cart ํ…Œ์ด๋ธ” ์ƒ์„ฑ ํ›„ alter ๋ช…๋ น์–ด๋กœ ์™ธ๋ž˜ํ‚ค(FK)๋ฅผ ๋งˆ์ง€๋ง‰์— ์ง€์ • ํ–ˆ๋‹ค



๐Ÿ“CartRepository ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ

package com.shop.repository;

import com.shop.entity.Cart;
import org.springframework.data.jpa.repository.JpaRepository;


// ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์กฐํšŒ๋ฅผ ์œ„ํ•œ ์ฟผ๋ฆฌ๋ฌธ ๋‚ ๋ฆฌ๋Š” JpaRepository
public interface CartRepository extends JpaRepository<Cart, Long> {

}



๐Ÿคฆโ€โ™€๏ธCart Entity ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ

package com.shop.entity;

import com.shop.dto.MemberFormDto;
import com.shop.repository.CartRepository;
import com.shop.repository.MemberRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;


@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class cartTest {

    @Autowired
    CartRepository cartRepository;

    @Autowired
    MemberRepository memberRepository;

    @Autowired
    PasswordEncoder passwordEncoder;

    @PersistenceContext // Entity ์˜์†์„ฑ ์ €์žฅ ํ™˜๊ฒฝ
    EntityManager em;

    public Member createMember(){

        MemberFormDto memberFormDto = new MemberFormDto();
        memberFormDto.setEmail("test@email.com");
        memberFormDto.setName("ํ™๊ธธ๋™");
        memberFormDto.setAddress("์„œ์šธ์‹œ ๋งˆํฌ๊ตฌ ํ•ฉ์ •๋™");
        memberFormDto.setPassword("1234");

        return Member.createMember(memberFormDto, passwordEncoder);

    }

    @Test
    @DisplayName("์žฅ๋ฐ”๊ตฌ๋‹ˆ ํšŒ์› ์—”ํ‹ฐํ‹ฐ ๋งคํ•‘ ์กฐํšŒ ํ…Œ์ŠคํŠธ")
    public void findCartAndMemberTest(){
        Member member = createMember();
        memberRepository.save(member);

        Cart cart = new Cart();
        cart.setMember(member);
        cartRepository.save(cart);
        
        em.flush(); // ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅ ํ›„ ํŠธ๋žœ์žญ์…˜์ด ๋๋‚  ๋•Œ flush() ํ˜ธ์ถœํ•˜์—ฌ DB์— ๋ฐ˜์˜
        em.clear(); // ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ์กฐํšŒ ํ›„ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค๋ฅผ ์กฐํšŒ, ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ๋น„์šด๋‹ค
        
        // Member ํ…Œ์ด๋ธ”์— ์ €์žฅ ๋˜๊ณ  Cart ํ…Œ์ด๋ธ”์— ์ €์žฅ
        /*
            findById select * from cart where id = ?
            ์œ„ ์ฟผ๋ฆฌ๋ฌธ์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด EntityNotFoundException Throw
            orElseThrow ๐Ÿ‘‰ EntityNotFoundException(๊ฒฐ๊ณผ๊ฐ€ ์—†์„๋•Œ)
            ์œ„ ์ฟผ๋ฆฌ๋ฌธ์—์„œ ๋ฌธ์ œ๊ฐ€ ์—†์œผ๋ฉด Cart(Entity)
         */
        Cart savedCart = cartRepository.findById(cart.getId()).orElseThrow(EntityNotFoundException::new);
        // savedCart์˜ member_id์™€ member_id๊ฐ€ ๊ฐ™๋‚˜ ๋น„๊ต
        assertEquals(savedCart.getMember().getId(), member.getId());

    }

}



๐Ÿ“๊ฒฐ๊ณผ

select
        c1_0.cart_id,
        m1_0.member_id,
        m1_0.address,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role,
        m1_0.tel_number 
    from
        cart c1_0 
    left join
        member m1_0 
            on m1_0.member_id=c1_0.member_id 
    where
        c1_0.cart_id=?

๐Ÿ‘‰ left ์กฐ์ธ ์„ฑ๊ณต, ์ฆ‰์‹œ ๋กœ๋”ฉ์œผ๋กœ Cart ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•  ๋•Œ, ๋งคํ•‘๋œ ์—”ํ‹ฐํ‹ฐ๋„ ํ•œ ๋ฒˆ์— ์กฐํšŒ(default) ๐Ÿ‘‰ @OnetoOne(fetch = FetchType.EAGER) ์ด ๋””ํดํŠธ๋ผ๋Š” ๊ฒƒ. ๋งŽ์ด ์“ฐ๋Š” ๊ฒƒ์€ ์ง€์—ฐ ๋กœ๋”ฉ LAZY. ๋‚˜์ค‘์— ๋‚˜์˜ฌ ์˜ˆ์ •.



๐Ÿ“Œ1:N/N:1 ๋‹จ๋ฐฉํ–ฅ ๊ด€๊ณ„

cart(์žฅ๋ฐ”๊ตฌ๋‹ˆ) โฌ… cart_item(์žฅ๋ฐ”๊ตฌ๋‹ˆ ์•ˆ์˜ ์ƒํ’ˆ) โžก item (ํŒ๋งค ์ƒํ’ˆ)
ํ•˜๋‚˜์˜ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์—ฌ๋Ÿฌ๊ฐœ์˜ ์ƒํ’ˆ์„ ๋‹ด๋Š” ๊ฒƒ


๐Ÿ“CartItem Entity ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name = "cart_item")
public class CartItem {

    @Id
    @GeneratedValue
    @Column(name = "cart_item_id")
    private Long id;

    // 1 : N
    @ManyToOne // CartItem : Cart, ํด๋ž˜์Šค : ๋ณ€์ˆ˜ ๐Ÿ‘‰ ์ด๋Ÿฐ ์‹์œผ๋กœ ์•Œ๋ฉด ํŽธํ•˜๋‹ค.
    @JoinColumn(name = "cart_id")
    private Cart cart;

    // N : 1
    @ManyToOne // CartItem : Item, ํด๋ž˜์Šค : ๋ณ€์ˆ˜ ๐Ÿ‘‰ ์ด๋Ÿฐ ์‹์œผ๋กœ ์•Œ๋ฉด ํŽธํ•˜๋‹ค.
    @JoinColumn(name = "item_id")
    private Item item;

    // ์ƒํ’ˆ ๊ฐœ์ˆ˜ count
    private int count;

}



๐Ÿ“๊ฒฐ๊ณผ

๐Ÿ‘‰ cart ํ…Œ์ด๋ธ” ์ƒ์„ฑ

๐Ÿ‘‰ cart_item ํ…Œ์ด๋ธ” ์ƒ์„ฑ

๐Ÿ‘‰ item ํ…Œ์ด๋ธ” ์ƒ์„ฑ

๐Ÿ‘‰ alter ๋ช…๋ น์–ด๋กœ ๊ฐ๊ฐ ์™ธ๋ž˜ํ‚ค(FK) ์ง€์ •



๐Ÿ“Œ1:N/N:1 ๋‹จ๋ฐฉํ–ฅ ๊ด€๊ณ„ 2

order โžก member
ํšŒ์› ํ•œ๋ช…์ด ์ฃผ๋ฌธ์„ ์—ฌ๋Ÿฌ๊ฐœ ํ•œ๋‹ค


๐Ÿ“OrderStatus Enum ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.constant;

public enum OrderStatus {
    
    // ์ฃผ๋ฌธ, ์ทจ์†Œ. ํ™˜๋ถˆ ๊ฐ™์€ ๊ฑด ๋‚˜์ค‘์—
    ORDER, CANCEL
}



๐Ÿ“Order Entity ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.entity;

import com.shop.constant.OrderStatus;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import javax.annotation.processing.Generated;
import java.time.LocalDateTime;

@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {

    @Id
    @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus;
    
    private LocalDateTime regTime;
    
    private LocalDateTime updateTime;


}



๐Ÿ“๊ฒฐ๊ณผ

๐Ÿ‘‰ alter ๋ช…๋ น์–ด๋กœ ๊ฐ๊ฐ ์™ธ๋ž˜ํ‚ค(FK) ์ง€์ •



๐Ÿคฆโ€โ™€๏ธ MySQL ๋‹ค์ด์–ด ๊ทธ๋žจ ๋งŒ๋“ค๊ธฐ

๐Ÿ‘‰ ์ค‘๊ฐ„์— ๋น„๋ฐ€๋ฒˆํ˜ธ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

๐Ÿ‘‰ ์™ธ๋ž˜ํ‚ค

๐Ÿ‘‰ ๊ธฐ๋ณธํ‚ค

๐Ÿ‘‰ ํ…Œ์ด๋ธ”์„ ๋ˆŒ๋Ÿฌ๊ฐ€๋ฉฐ ์–ด๋–ค ๊ฒƒ์ด ์‚ฌ์šฉ๋˜๋Š”์ง€ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ๋‹ค



๐Ÿ“ŒN:M ๋‹จ๋ฐฉํ–ฅ ๊ด€๊ณ„

member โ†” item
1:N โ†” N:1 ํ˜•ํƒœ๋กœ ๊ต์ฐจ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ž๋™์œผ๋กœ ๋งŒ๋“ ๋‹ค. ์ด๋Ÿฐ ์‹์œผ๋กœ ์—ฐ๊ฒฐ ๋œ ํ…Œ์ด๋ธ”์—๋Š” ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์—†์–ด ์‚ฌ์šฉ ํ•  ์ผ์€ ๋งŽ์ง€ ์•Š๋‹ค.

๐Ÿ“Item Entity ํด๋ž˜์Šค ์ˆ˜์ •

    private LocalDateTime updateTime; // ์ˆ˜์ • ์‹œ๊ฐ„

    @ManyToMany
    // ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์„ ๋„ฃ๋Š” ๊ณผ์ •
    @JoinTable(
            name = "member_item",
            joinColumns = @JoinColumn(name = "member_id"),
            inverseJoinColumns = @JoinColumn(name = "item_id")
    )
    private List<Member> member; // Member์—์„œ ์™ธ๋ž˜ํ‚ค๋ฅผ ๊ฐ€์ ธ ์™”์œผ๋‹ˆ Member๊ฐ€ ์ฃผ์ธ
}






๐Ÿ“Œ์–‘๋ฐฉํ–ฅ ๊ด€๊ณ„

์—”ํ‹ฐํ‹ฐ๋ฅผ ์–‘๋ฐฉํ–ฅ์œผ๋กœ ์—ฐ๊ด€ ๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•˜๋ฉด ๊ฐ์ฒด์˜ ์ฐธ์กฐ๋Š” ๋‘˜์ธ๋ฐ ์™ธ๋ž˜ํ‚ค๋Š” ํ•˜๋‚˜์ธ ๊ด€๊ณ„.
์ฆ‰, ์™ธ๋ž˜ํ‚ค๋ฅผ ์„œ๋กœ ๊ฐ–๊ณ  ์žˆ๋‹ค ๐Ÿ‘‰ ์ฃผ์ธ๊ณผ ๋ถ€ํ•˜ ๊ด€๊ณ„๋ฅผ ์ •๋ฆฝํ•˜๊ธฐ ์‰ฝ์ง€ ์•Š๋‹ค.
Java์—์„œ๋Š” List<>๋“ฑ ์„ ์ถ”๊ฐ€ํ•˜๋ฉฐ ๊ด€๊ณ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š”๋ฐ DB ์—์„œ๋Š” ๋”ฐ๋กœ ๋‚˜ํƒ€๋‚ผ ๊ฒƒ์ด ์—†๊ณ  ๊ทธ๋ƒฅ ๋„ฃ์œผ๋ฉด ๋œ๋‹ค.

์ฃผ์ธ๊ณผ ๋ถ€ํ•˜๋ฅผ ์ •ํ•˜๋Š” ๊ทœ์น™

  • ์—ฐ๊ด€ ๊ด€๊ณ„์˜ ์ฃผ์ธ(PK)์€ ์™ธ๋ž˜ํ‚ค(FK)๊ฐ€ ์žˆ๋Š” ๊ณณ์œผ๋กœ ์„ค์ •
  • ์—ฐ๊ด€ ๊ด€๊ณ„์˜ ์ฃผ์ธ(PK)์ด ์™ธ๋ž˜ํ‚ค(FK)๋ฅผ ๊ด€๋ฆฌ(๋“ฑ๋ก, ์ˆ˜์ •, ์‚ญ์ œ)
  • ๋ถ€ํ•˜๋Š” ์—ฐ๊ด€ ๊ด€๊ณ„ ๋งคํ•‘ ์‹œ mappedBy ์†์„ฑ ๊ฐ’์œผ๋กœ ์ฃผ์ธ ์ง€์ •. ์ฝ๊ธฐ๋งŒ ๊ฐ€๋Šฅํ•˜๋‹ค ๐Ÿ‘‰ ์„œ๋กœ ์™ธ๋ž˜ํ‚ค๋ฅผ ๊ฐ–๊ณ  ์žˆ์œผ๋ฏ€๋กœ mappedBy๋ฅผ ๊ฐ–๋Š” ์ชฝ์ด ๋ถ€ํ•˜
    ๐Ÿ‘‰ ์ฃผ์ธ์€ mappedBy ์†์„ฑ ์‚ฌ์šฉ โŒ, ๋ถ€ํ•˜๋Š” mappedBy ์†์„ฑ์œผ๋กœ ์ฃผ์ธ ์ง€์ •

    โ“๐Ÿ“ mappedBy๋ฅผ ์ ์–ด์ฃผ๋Š” ์ด์œ ๋Š” ๋ญ˜๊นŒ? (by ๊น€์˜ํ•œ)

    ๊ฐ์ฒด์™€ ํ…Œ์ด๋ธ”๊ฐ„์˜ ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ๋งบ๋Š” ์ฐจ์ด๋ฅผ ์ดํ•ดํ•ด์•ผ ํ•œ๋‹ค

    MEMBER๊ณผ TEAM์„ ์˜ˆ์‹œ๋กœ ๋“ค์–ด๋ณด๋ฉด

    ๊ฐ์ฒด ์—ฐ๊ด€ ๊ด€๊ณ„๋Š”
    ํšŒ์› โžก ํŒ€ ์—ฐ๊ด€๊ด€๊ณ„ 1๊ฐœ (๋‹จ๋ฐฉํ–ฅ)
    ํŒ€ โžก ํšŒ์› ์—ฐ๊ด€๊ด€๊ณ„ 1๊ฐœ (๋‹จ๋ฐฉํ–ฅ) ์ด๋ฉฐ
    ํ…Œ์ด๋ธ” ์—ฐ๊ด€ ๊ด€๊ณ„๋Š”
    ํšŒ์› โ†” ํŒ€ ์—ฐ๊ด€๊ด€๊ฒŒ๋กœ ์–‘๋ฐฉํ–ฅ ์ด๋‹ค.

    ์—ฌ๊ธฐ์„œ ์ฐจ์ด์ ์€ ๊ฐ์ฒด์˜ ์–‘๋ฐฉํ–ฅ ๊ด€๊ณ„๋Š” ์‚ฌ์‹ค ์–‘๋ฐฉํ–ฅ ๊ด€๊ณ„๊ฐ€ ์•„๋‹ˆ๋ผ ์„œ๋กœ ๋‹ค๋ฅธ ๋‹จ๋ฐฉํ–ฅ ๊ด€๊ณ„ 2๊ฐœ ์ด๋‹ค. ์ฆ‰, ๊ฐ์ฒด๋ฅผ ์–‘๋ฐฉํ–ฅ์œผ๋กœ ์ฐธ์กฐํ•˜๋ ค๋ฉด ๋‹จ๋ฐฉํ–ฅ ์—ฐ๊ด€ ๊ด€๊ณ„ 2๊ฐœ๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค( a.getB(), b.getA() ๐Ÿ‘‰ use ๊ด€๊ณ„ )
    ํ•˜์ง€๋งŒ ํ…Œ์ด๋ธ”์˜ ์–‘๋ฐฉํ–ฅ ๊ด€๊ณ„๋Š” ์™ธ๋ž˜ํ‚ค ํ•˜๋‚˜๋กœ ๋‘ ํ…Œ์ด๋ธ”์˜ ์—ฐ๊ด€ ๊ด€๊ณ„๋ฅผ ๊ด€๋ฆฌํ•œ๋‹ค. MEMBER.TEAM_ID ์™ธ๋ž˜ํ‚ค ํ•˜๋‚˜๋กœ ์–‘๋ฐฉํ–ฅ ์—ฐ๊ด€ ๊ด€๊ณ„๋ฅผ ๊ฐ–๋Š”๋‹ค. ์ฆ‰, ์–‘์ชฝ์œผ๋กœ Join์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

    ๊ทธ๋Ÿผ ์–ด๋–ค ์™ธ๋ž˜ํ‚ค๋กœ ๊ด€๋ฆฌ๋ฅผ ํ•ด์•ผํ• ๊นŒ? TEAM.MEMBER_ID ๋กœ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ• ๊นŒ? ๋‘˜ ์ค‘์— ๋ญ˜ ๋ฏฟ์–ด์•ผํ•ด? TEAM์„ ์—…๋ฐ์ดํŠธ ํ–ˆ๋Š”๋ฐ ์™œ ์•ˆ๋˜๋Š”๊ฑฐ์•ผ?? ์™œ MEMBER๊ฐ€ ๋ฐ”๋€Œ์ง€?? ์•„๋‹ˆ ์™œ ๊ฐ’์ด ์•ˆ๋“ค์–ด๊ฐ€?? ๋ฐ˜๋Œ€๋กœํ•˜๋‹ˆ๊นŒ ๊ฐ’์ด ๋“ค์–ด๊ฐ€๋Š”๋ฐ?? ๋ฒ„๊ทผ๊ฐ€???

    ๐Ÿ‘‰ ๊ทธ๋ž˜์„œ ๊ทœ์น™์„ ํ•˜๋‚˜ ์ •ํ–ˆ๋‹ค. ๋‘˜๋‹ค ๋™์‹œ์— ๋“ค์–ด๊ฐ€๋ฉด ์—ฐ๊ด€ ๊ด€๊ณ„์˜ ์ฃผ์ธ๋งŒ ๊ด€๋ฆฌ๋ฅผ ํ•œ๋‹ค.

    ๐Ÿ‘‰ ์ฃผ์ธ๊ณผ ๋ถ€ํ•˜๋ฅผ ์ •ํ•˜๋Š” ๊ทœ์น™ ์„ mappedBy ํ•˜๊ธฐ๋กœ ํ•œ ๊ฒƒ

    โ“๊ทธ๋ž˜์„œ ๋ˆ„๊ตฌ๋ฅผ ์ฃผ์ธ์œผ๋กœ ํ•ด์•ผํ•˜๋Š”๋ฐ?

    ๋‹ต์€ 99% ์ •ํ•ด์ ธ ์žˆ๋‹ค. ์™ธ๋ž˜ํ‚ค๋ฅผ ๊ฐ–๊ณ  ์žˆ๋Š” ๊ณณ์„ ์ฃผ์ธ์œผ๋กœ ์ •ํ•˜์ž. ์ธ์ง€๋ถ€์กฐํ™”๋ฅผ ์ฐจ๋‹จํ•˜์ž.

    ๐Ÿ‘‰ ์—ฌ๊ธฐ์„œ๋Š” Member.team์ด ์—ฐ๊ด€๊ด€๊ณ„์˜ ์ฃผ์ธ.
    ์ผ๋‹จ ๋‹จ๋ฐฉํ–ฅ ๋งคํ•‘์œผ๋กœ ๊ฐœ๋ฐœ์„ ํ•˜๊ณ  ์–‘๋ฐฉํ–ฅ์ด ํ•„์š”ํ•  ๋•Œ Java์— ๋ช‡์ค„ ์ถ”๊ฐ€ํ•ด ์ค˜์„œ(DB์—๋Š” ์˜ํ–ฅ์ด ์—†์œผ๋‹ˆ) ์ถ”๊ฐ€ํ•ด ์ฃผ๋ฉด ๋œ๋‹ค.

    ์ฆ‰, ๋‹จ๋ฐฉํ–ฅ ๋งคํ•‘๋งŒ์œผ๋กœ๋„ ORM ๋งคํ•‘์„ ๋๋‚ผ ์ˆ˜ ์žˆ๋‹ค. ์–‘๋ฐฉํ–ฅ ๋งคํ•‘์€ ๋‹จ์ˆœํ•˜๊ฒŒ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์„ ํŽธํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€ํ•˜๋Š” ๋ถ€๊ฐ€์ ์ธ ๊ธฐ๋Šฅ์ด๋ผ ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

    ๊ฐ’์„ ์ž…๋ ฅํ•  ๋• ์ฃผ์ธ์— ๊ฐ’์„ ์ž…๋ ฅํ•˜๋ฉด ๋˜๋Š”๋ฐ ์ˆœ์ˆ˜ํ•œ ๊ฐ์ฒด ๊ด€๊ณ„๋ฅผ ๊ณ ๋ คํ•œ๋‹ค๋ฉด ํ•ญ์ƒ ์–‘์ชฝ ๋‹ค ๊ฐ’์„ ์ž…๋ ฅํ•ด์•ผ ํ•œ๋‹ค.

์–‘๋ฐฉํ–ฅ ๋งคํ•‘์˜ ์žฅ์ 

  • ๋‹จ๋ฐฉํ–ฅ ๋งคํ•‘๋งŒ์œผ๋กœ๋„ ์ด๋ฏธ ์—ฐ๊ด€๊ด€๊ณ„ ๋งคํ•‘์€ ์™„๋ฃŒ
  • ์–‘๋ฐฉํ–ฅ ๋งคํ•‘์€ ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์œผ๋กœ ์กฐํšŒ(๊ฐ์ฒด ๊ทธ๋ž˜ํ”„ ํƒ์ƒ‰) ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€ ๋œ ๊ฒƒ ๋ฟ์ด๋‹ค
  • JPQL์—์„œ ์—ญ๋ฐฉํ–ฅ์œผ๋กœ ํƒ์ƒ‰ํ•  ์ผ์ด ๋งŽ๋‹ค
  • ๋‹จ๋ฐฉํ–ฅ ๋งคํ•‘์„ ์ž˜ ํ•˜๊ณ  ์–‘๋ฐฉํ–ฅ์€ ํ•„์š”ํ•  ๋•Œ ์ถ”๊ฐ€ํ•ด๋„ ๋œ๋‹ค.

member โฌ… orders โ†” order_item

๐Ÿ‘‰ ํ•œ ๋ช…์˜ ํšŒ์›์€ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฃผ๋ฌธ ๊ฐ€๋Šฅํ•˜๋ฉฐ ํ•œ ๋ฒˆ์˜ ์ฃผ๋ฌธ์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ƒํ’ˆ์„ ์ฃผ๋ฌธ ๊ฐ€๋Šฅ


๐Ÿ“OrderItem Entity ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;

@Entity
@Getter
@Setter
// ํ…Œ์ด๋ธ”๋ช…์ด ์—†์œผ๋ฉด ์•Œ์•„์„œ ๋งŒ๋“ฆ
public class OrderItem {
    @Id
    @GeneratedValue
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "item_id") // ์™ธ๋ž˜ํ‚ค
    private Item item;

    @ManyToOne
    @JoinColumn(name = "order_id") // ์™ธ๋ž˜ํ‚ค
    private Order order;

    private int orderPrice;

    private int count;

    private LocalDateTime regTime;

    private LocalDateTime updateTime;
}



๐Ÿ“Order Entity ํด๋ž˜์Šค ์ˆ˜์ •

...

    @ManyToOne
    @JoinColumn(name = "member_id") // Order : Member
    private Member member;

    // ์„œ๋กœ ์™ธ๋ž˜ํ‚ค๋ฅผ ๊ฐ–๊ณ  ์žˆ์ง€๋งŒ
    // mappedBy๋กœ ์ฃผ์ธ/๋ถ€ํ•˜ ๊ด€๊ณ„๋ฅผ ์ง€์ •. ์ฆ‰, order_item์ด ์ฃผ์ธ
    // Join ์ปฌ๋Ÿผ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค
    @OneToMany(mappedBy = "order") // order_item ํ…Œ์ด๋ธ”์˜ order ํ•„๋“œ์— ๋งคํ•‘
    private List<OrderItem> orderItems = new ArrayList<>(); 

    private LocalDateTime orderDate;
    
...









๐Ÿ‘โ€๐Ÿ—จ์‡ผํ•‘๋ชฐ ํ”„๋กœ์ ํŠธ ERD

0๊ฐœ์˜ ๋Œ“๊ธ€