๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๋ฐฑ์—”๋“œ/spring&springboot

[๋‚ด์ผ๋ฐฐ์›€์บ ํ”„_Spring Boot ์ˆ™๋ จ์ฃผ์ฐจ] My Select Shop(ํšŒ์›๊ธฐ๋Šฅ, ์ƒํ’ˆ ํŽ˜์ด์ง• ๋ฐ ์ •๋ ฌ ๊ตฌํ˜„)

by alswlfl 2026. 6. 30.

ํšŒ์› ๊ธฐ๋Šฅ ๊ตฌํ˜„

ํšŒ์› ๊ด€๋ จ API ์„ค๊ณ„

๊ธฐ๋Šฅ Method URL ์„ค๋ช…
๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ GET /api/user/login-page ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ํ˜ธ์ถœ
ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€ GET /api/user/signup ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€ ํ˜ธ์ถœ
ํšŒ์›๊ฐ€์ž… POST /api/user/signup ํšŒ์›๊ฐ€์ž…
ํšŒ์› ์ •๋ณด ์š”์ฒญ GET /api/user-info ํšŒ์› ๊ด€๋ จ ์ •๋ณด ๋ฐ›๊ธฐ

ํšŒ์›๊ฐ€์ž… API ๊ตฌํ˜„

  • ํšŒ์› Entity์™€ Enum ์ถ”๊ฐ€: entity > User, UserEnum
@Getter
@Setter
@NoArgsConstructor
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    private UserRoleEnum role;

    public User(String username, String password, String email, UserRoleEnum role) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.role = role;
    }
}
package com.sparta.myselectshop.entity;

public enum UserRoleEnum {
    USER(Authority.USER),  // ์‚ฌ์šฉ์ž ๊ถŒํ•œ
    ADMIN(Authority.ADMIN);  // ๊ด€๋ฆฌ์ž ๊ถŒํ•œ

    private final String authority;

    UserRoleEnum(String authority) {
        this.authority = authority;
    }

    public String getAuthority() {
        return this.authority;
    }

    public static class Authority {
        public static final String USER = "ROLE_USER";
        public static final String ADMIN = "ROLE_ADMIN";
    }
}
  • controller > UserController ์ถ”๊ฐ€
@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/api")
public class UserController {

    private final UserService userService;

    @GetMapping("/user/login-page")
    public String loginPage() {
        return "login";
    }

    @GetMapping("/user/signup")
    public String signupPage() {
        return "signup";
    }

    @PostMapping("/user/signup")
    public String signup(@Valid SignupRequestDto requestDto, BindingResult bindingResult) {
        // Validation ์˜ˆ์™ธ์ฒ˜๋ฆฌ
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        if(fieldErrors.size() > 0) {
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                log.error(fieldError.getField() + " ํ•„๋“œ : " + fieldError.getDefaultMessage());
            }
            return "redirect:/api/user/signup";
        }

        userService.signup(requestDto);

        return "redirect:/api/user/login-page";
    }

    // ํšŒ์› ๊ด€๋ จ ์ •๋ณด ๋ฐ›๊ธฐ
    @GetMapping("/user-info")
    @ResponseBody
    public UserInfoDto getUserInfo(@AuthenticationPrincipal UserDetailsImpl userDetails) {
        String username = userDetails.getUser().getUsername();
        UserRoleEnum role = userDetails.getUser().getRole();
        boolean isAdmin = (role == UserRoleEnum.ADMIN);

        return new UserInfoDto(username, isAdmin);
    }
}
  • dto > SignupRequestDto, UserInfoDto ์ถ”๊ฐ€
@Getter
@Setter
public class SignupRequestDto {
    @NotBlank
    private String username;
    @NotBlank
    private String password;
    @Email
    @NotBlank
    private String email;
    private boolean admin = false;
    private String adminToken = "";
}
@Getter
@AllArgsConstructor
public class UserInfoDto {
    String username;
    boolean isAdmin;
}
  • service > UserService ์ƒ์„ฑ
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    // ADMIN_TOKEN
    private final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";

    public void signup(SignupRequestDto requestDto) {
        String username = requestDto.getUsername();
        String password = passwordEncoder.encode(requestDto.getPassword());

        // ํšŒ์› ์ค‘๋ณต ํ™•์ธ
        Optional<User> checkUsername = userRepository.findByUsername(username);
        if (checkUsername.isPresent()) {
            throw new IllegalArgumentException("์ค‘๋ณต๋œ ์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.");
        }

        // email ์ค‘๋ณตํ™•์ธ
        String email = requestDto.getEmail();
        Optional<User> checkEmail = userRepository.findByEmail(email);
        if (checkEmail.isPresent()) {
            throw new IllegalArgumentException("์ค‘๋ณต๋œ Email ์ž…๋‹ˆ๋‹ค.");
        }

        // ์‚ฌ์šฉ์ž ROLE ํ™•์ธ
        UserRoleEnum role = UserRoleEnum.USER;
        if (requestDto.isAdmin()) {
            if (!ADMIN_TOKEN.equals(requestDto.getAdminToken())) {
                throw new IllegalArgumentException("๊ด€๋ฆฌ์ž ์•”ํ˜ธ๊ฐ€ ํ‹€๋ ค ๋“ฑ๋ก์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.");
            }
            role = UserRoleEnum.ADMIN;
        }

        // ์‚ฌ์šฉ์ž ๋“ฑ๋ก
        User user = new User(username, password, email, role);
        userRepository.save(user);
    }
}
  • repository > UserRepository ์ƒ์„ฑ
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    Optional<User> findByEmail(String email);
}

JWT ๋กœ๊ทธ์ธ ์ธ์ฆ ์ฒ˜๋ฆฌ(Filter)

  • ์ด์ „๊นŒ์ง€๋Š” JWT๋ฅผ ์ƒ์„ฑํ•œ ์„œ๋ฒ„์—์„œ ์ฟ ํ‚ค๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•ด Client๋กœ ์ „๋‹ฌํ•จ → ์‘๋‹ต Header์— Set-Cookie๋กœ ๊ฐ’์ด ์ „๋‹ฌ๋˜์–ด, Client์—์„œ ์ฟ ํ‚ค๋ฅผ ์ง์ ‘ ์ฟ ํ‚ค ์ €์žฅ์†Œ์— ์ €์žฅํ•˜์ง€ ์•Š์•„๋„ ์ž๋™์œผ๋กœ ๊ฐ’์ด ์ €์žฅ๋จ
  • ๋˜ํ•œ, Client์—์„œ ๋”ฐ๋กœ Header์— JWT๋ฅผ ์ง์ ‘ ๋‹ด์•„์„œ ๋ณด๋‚ด์ง€ ์•Š๊ณ  ์„œ๋ฒ„์—์„œ Request์— ๋‹ด๊ธด ์ฟ ํ‚ค ๊ฐ’๋“ค ์ค‘ JWT์— ํ•ด๋‹นํ•˜๋Š” ๊ฐ’์„ ๊ฐ€์ ธ์™€ ์‚ฌ์šฉํ•จ
  • Client์™€ Server๊ฐ€ JWT๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐฉ์‹์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ์„ ํƒํ•จ
  • ์ด๋ฒˆ์—๋Š” Client์™€ Server ๋ชจ๋‘ JWT๋ฅผ ์ง์ ‘ HTTP Header์— ๋‹ด์•„์„œ ์ „๋‹ฌ๋ฐ›๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„

JWT ์‚ฌ์šฉ ํ๋ฆ„

1. Client๊ฐ€ username๊ณผ password๋กœ ๋กœ๊ทธ์ธ ์„ฑ๊ณต์‹œ,
1-1) ์„œ๋ฒ„์—์„œ "๋กœ๊ทธ์ธ ์ •๋ณด"๋ฅผ JWT๋กœ ์•”ํ˜ธํ™”(Secret Key ์‚ฌ์šฉ)

 

1-2) JWT๋ฅผ Client ์‘๋‹ต Header์— ์ „๋‹ฌ

Authorization: Bearer <JWT>

 

1-3) Client์—์„œ JWT ์ €์žฅ(์ฟ ํ‚ค)

 

2. Client์—์„œ JWT ํ†ตํ•ด ์ธ์ฆ ๋ฐฉ๋ฒ•

2-1) JWT๋ฅผ API ์š”์ฒญ ์‹œ๋งˆ๋‹ค Header์— ํฌํ•จ

ex) HTTP Headers

Content-Type: application/json
Authorization: Bearer <JWT>
...

 

2-2) Server

  • JWT ์œ„์กฐ ์—ฌ๋ถ€ ๊ฒ€์ฆ(Secret Key ์‚ฌ์šฉ)
  • JWT ์œ ํšจ๊ธฐ๊ฐ„์ด ์ง€๋‚˜์ง€ ์•Š์•˜๋Š”์ง€ ๊ฒ€์ฆ
  • ๊ฒ€์ฆ ์„ฑ๊ณต์‹œ, JWT์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์™€ ํ™•์ธ

๋กœ๊ทธ์ธ ์ธ์ฆ ์ฒ˜๋ฆฌ ๊ตฌํ˜„

jwt > JwtUtil ์ƒ์„ฑ

@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
    // Header KEY ๊ฐ’
    public static final String AUTHORIZATION_HEADER = "Authorization";
    // ์‚ฌ์šฉ์ž ๊ถŒํ•œ ๊ฐ’์˜ KEY
    public static final String AUTHORIZATION_KEY = "auth";
    // Token ์‹๋ณ„์ž
    public static final String BEARER_PREFIX = "Bearer ";
    // ํ† ํฐ ๋งŒ๋ฃŒ์‹œ๊ฐ„
    private final long TOKEN_TIME = 60 * 60 * 1000L; // 60๋ถ„

    @Value("${jwt.secret.key}") // Base64 Encode ํ•œ SecretKey
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // ํ† ํฐ ์ƒ์„ฑ
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username) // ์‚ฌ์šฉ์ž ์‹๋ณ„์ž๊ฐ’(ID)
                        .claim(AUTHORIZATION_KEY, role) // ์‚ฌ์šฉ์ž ๊ถŒํ•œ
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // ๋งŒ๋ฃŒ ์‹œ๊ฐ„
                        .setIssuedAt(date) // ๋ฐœ๊ธ‰์ผ
                        .signWith(key, signatureAlgorithm) // ์•”ํ˜ธํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜
                        .compact();
    }

    // header ์—์„œ JWT ๊ฐ€์ ธ์˜ค๊ธฐ
    public String getJwtFromHeader(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7);
        }
        return null;
    }

    // ํ† ํฐ ๊ฒ€์ฆ
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException | SignatureException e) {
            log.error("Invalid JWT signature, ์œ ํšจํ•˜์ง€ ์•Š๋Š” JWT ์„œ๋ช… ์ž…๋‹ˆ๋‹ค.");
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token, ๋งŒ๋ฃŒ๋œ JWT token ์ž…๋‹ˆ๋‹ค.");
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token, ์ง€์›๋˜์ง€ ์•Š๋Š” JWT ํ† ํฐ ์ž…๋‹ˆ๋‹ค.");
        } catch (IllegalArgumentException e) {
            log.error("JWT claims is empty, ์ž˜๋ชป๋œ JWT ํ† ํฐ ์ž…๋‹ˆ๋‹ค.");
        }
        return false;
    }

    // ํ† ํฐ์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
}

 

security > UserDetailsImpl๊ณผ UserDetailsServiceImpl ์ƒ์„ฑ

public class UserDetailsImpl implements UserDetails {

    private final User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

๋กœ๊ทธ์ธ ์‹œ ์‚ฌ์šฉํ•  Dto ์ƒ์„ฑ, dto > LoginRequestDto

@Setter
@Getter
public class LoginRequestDto {
    private String username;
    private String password;
}

 

์ธ์ฆ/์ธ๊ฐ€ Filter ์ƒ์„ฑ, security > JwtAuthenticationFilter, JwtAuthorizationFilter ์ถ”๊ฐ€

@Slf4j(topic = "๋กœ๊ทธ์ธ ๋ฐ JWT ์ƒ์„ฑ")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
        setFilterProcessesUrl("/api/user/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.getPassword(),
                            null
                    )
            );
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
        String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

        String token = jwtUtil.createToken(username, role);
        response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        response.setStatus(401);
    }

}
@Slf4j(topic = "JWT ๊ฒ€์ฆ ๋ฐ ์ธ๊ฐ€")
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;

    public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {

        String tokenValue = jwtUtil.getJwtFromHeader(req);

        if (StringUtils.hasText(tokenValue)) {

            if (!jwtUtil.validateToken(tokenValue)) {
                log.error("Token Error");
                return;
            }

            Claims info = jwtUtil.getUserInfoFromToken(tokenValue);

            try {
                setAuthentication(info.getSubject());
            } catch (Exception e) {
                log.error(e.getMessage());
                return;
            }
        }

        filterChain.doFilter(req, res);
    }

    // ์ธ์ฆ ์ฒ˜๋ฆฌ
    public void setAuthentication(String username) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(username);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }

    // ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑ
    private Authentication createAuthentication(String username) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

 

config ์„ค์ •, config > WebSecurityConfig

@Configuration
@EnableWebSecurity // Spring Security ์ง€์›์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    private final AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
        filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
        return filter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF ์„ค์ •
        http.csrf((csrf) -> csrf.disable());

        // ๊ธฐ๋ณธ ์„ค์ •์ธ Session ๋ฐฉ์‹์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  JWT ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ •
        http.sessionManagement((sessionManagement) ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources ์ ‘๊ทผ ํ—ˆ์šฉ ์„ค์ •
                        .requestMatchers("/").permitAll() // ๋ฉ”์ธ ํŽ˜์ด์ง€ ์š”์ฒญ ํ—ˆ๊ฐ€
                        .requestMatchers("/api/user/**").permitAll() // '/api/user/'๋กœ ์‹œ์ž‘ํ•˜๋Š” ์š”์ฒญ ๋ชจ๋‘ ์ ‘๊ทผ ํ—ˆ๊ฐ€
                        .anyRequest().authenticated() // ๊ทธ ์™ธ ๋ชจ๋“  ์š”์ฒญ ์ธ์ฆ์ฒ˜๋ฆฌ
        );

        http.formLogin((formLogin) ->
                formLogin
                        .loginPage("/api/user/login-page").permitAll()
        );

        // ํ•„ํ„ฐ ๊ด€๋ฆฌ
        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

JWT ๊ธฐ๋Šฅ ํ˜ธ์ถœ ๊ฒฐ๊ณผ


ํšŒ์›๋ณ„ ์ƒํ’ˆ API ๊ตฌํ˜„

์ƒํ’ˆ๊ณผ ํšŒ์›์˜ ๊ด€๊ณ„

  • ๊ด€์‹ฌ ์ƒํ’ˆ ๋“ฑ๋ก ์‹œ, ๋“ฑ๋ก์„ ์š”์ฒญํ•œ "ํšŒ์› ์ •๋ณด" ์ถ”๊ฐ€ ํ•„์š”
  • ์ƒํ’ˆ : ํšŒ์› = N : 1

์ƒํ’ˆ๊ณผ ํšŒ์› ๊ด€๊ณ„

  • ํšŒ์› ๊ฐ์ฒด์—์„œ ์ƒํ’ˆ ๊ฐ์ฒด๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์—, ์ƒํ’ˆ๊ณผ ํšŒ์›์„ N : 1  ๋‹จ๋ฐฉํ–ฅ ์—ฐ๊ด€๊ด€๊ณ„๋กœ ์„ค์ •

Product ์—”ํ‹ฐํ‹ฐ์— User Entity์™€์˜ ๊ด€๊ณ„ ์„ค์ • ์ถ”๊ฐ€

@Entity // JPA๊ฐ€ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” Entity ํด๋ž˜์Šค ์ง€์ •
@Getter
@Setter
@Table(name = "product") // ๋งคํ•‘ํ•  ํ…Œ์ด๋ธ”์˜ ์ด๋ฆ„์„ ์ง€์ •
@NoArgsConstructor
public class Product extends Timestamped {
	...

    @ManyToOne(fetch = FetchType.LAZY) // Product ์กฐํšŒํ•  ๋•Œ๋งˆ๋‹ค ํšŒ์› ์ •๋ณด๊ฐ€ ์ „๋ถ€ ํ•„์š”ํ•œ๊ฑด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ง€์—ฐ๋กœ๋”ฉ ์„ค์ •
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

	...
}

 

ProductController ๋‚ด์— ๊ด€์‹ฌ ์ƒํ’ˆ ๋“ฑ๋ก API์™€ ๊ด€์‹ฌ ์ƒํ’ˆ ์กฐํšŒ API์— ํšŒ์› ์ •๋ณด ์ถ”๊ฐ€

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class ProductController {

    private final ProductService productService;

	// ์ƒํ’ˆ ๋“ฑ๋ก
    @PostMapping("/products")
    public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return productService.createProduct(requestDto, userDetails.getUser());
    }

    ...

	// ์ƒํ’ˆ ์กฐํšŒ
    @GetMapping("/products")
    public List<ProductResponseDto> getProducts(@AuthenticationPrincipal UserDetailsImpl userDetails) {
        return productService.getProducts(userDetails.getUser());
    }
}
  • Product ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ์ž์— User ์ •๋ณด ์ถ”๊ฐ€
@Getter
@Setter
@Table(name = "product") // ๋งคํ•‘ํ•  ํ…Œ์ด๋ธ”์˜ ์ด๋ฆ„์„ ์ง€์ •
@NoArgsConstructor
public class Product extends Timestamped {
	...
	@ManyToOne(fetch = FetchType.LAZY) // Product ์กฐํšŒํ•  ๋•Œ๋งˆ๋‹ค ํšŒ์› ์ •๋ณด๊ฐ€ ์ „๋ถ€ ํ•„์š”ํ•œ๊ฑด ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ง€์—ฐ๋กœ๋”ฉ ์„ค์ •
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    public Product(ProductRequestDto requestDto, User user) {
        this.title = requestDto.getTitle();
        this.image = requestDto.getImage();
        this.link = requestDto.getLink();
        this.lprice = requestDto.getLprice();
        this.user = user;
    }
    ...
}
@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;

    public static final int MIN_MY_PRICE = 100;

    public ProductResponseDto createProduct(ProductRequestDto requestDto, User user) {
        Product product = productRepository.save(new Product(requestDto, user));
        return new ProductResponseDto(product);
    }

    ...
    
    public List<ProductResponseDto> getProducts(User user) {
        List<Product> productList = productRepository.findAllByUser(user);
        List<ProductResponseDto> responseDtoList = new ArrayList<>();

        for (Product product : productList) {
            responseDtoList.add(new ProductResponseDto(product));
        }
        return responseDtoList;
    }

    ...
}

 

Admin ๊ณ„์ •์€ ๋ชจ๋“  ํšŒ์›์ด ๋“ฑ๋กํ•œ ๋ชจ๋“  ์ƒํ’ˆ ์กฐํšŒ API ๊ตฌํ˜„

public class ProductController {

    private final ProductService productService;

    ...

    @GetMapping("/admin/products")
    public List<ProductResponseDto> getAllProducts() {
        return productService.getAllProducts();
    }
}
public class ProductService {

    private final ProductRepository productRepository;
    
    ...

    public List<ProductResponseDto> getAllProducts() {
        List<Product> productList = productRepository.findAll();
        List<ProductResponseDto> responseDtoList = new ArrayList<>();

        for (Product product : productList) {
            responseDtoList.add(new ProductResponseDto(product));
        }
        return responseDtoList;
    }
}

ํšŒ์› ๋ณ„ ์ƒํ’ˆ ์กฐํšŒ API ํ˜ธ์ถœ ๊ฒฐ๊ณผ


์ƒํ’ˆ ํŽ˜์ด์ง• ๋ฐ ์ •๋ ฌ

ํŽ˜์ด์ง• ๋ฐ ์ •๋ ฌ ์„ค๊ณ„

Client → Server

  • ํŽ˜์ด์ง•
    • page: ์กฐํšŒํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ(1๋ถ€ํ„ฐ ์‹œ์ž‘)
    • size: ํ•œ ํŽ˜์ด์ง€์— ๋ณด์—ฌ์ค„ ์ƒํ’ˆ ๊ฐœ์ˆ˜(10๊ฐœ๋กœ ๊ณ ์ •)
  • ์ •๋ ฌ
    • sortBy(์ •๋ ฌ ํ•ญ๋ชฉ)
      - id: Product ํ…Œ์ด๋ธ”์˜ id
      - title: ์ƒํ’ˆ๋ช…
      - lprice: ์ตœ์ €๊ฐ€
    • isAsc(์˜ค๋ฆ„์ฐจ์ˆœ)
      - true: ์˜ค๋ฆ„์ฐจ์ˆœ(ASC)
      - false: ๋‚ด๋ฆผ์ฐจ์ˆœ(DESC)

Server → Client

  • number: ์กฐํšŒ๋œ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ(0๋ถ€ํ„ฐ ์‹œ์ž‘)
  • content: ์กฐํšŒ๋œ ์ƒํ’ˆ ์ •๋ณด(๋ฐฐ์—ด)
  • size: ํ•œ ํŽ˜์ด์ง€์— ๋ณด์—ฌ์ค„ ์ƒํ’ˆ ๊ฐœ์ˆ˜
  • numberOfElements: ์‹ค์ œ ์กฐํšŒ๋œ ์ƒํ’ˆ ๊ฐœ์ˆ˜
  • totalElements: ์ „์ฒด ์ƒํ’ˆ ๊ฐœ์ˆ˜(ํšŒ์›์ด ๋“ฑ๋กํ•œ ๋ชจ๋“  ์ƒํ’ˆ์˜ ๊ฐœ์ˆ˜)
  • totalPages: ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜
    - totalPages = totalElements / size ๊ฒฐ๊ณผ๋ฅผ ์†Œ์ˆ˜์  ์˜ฌ๋ฆผ
  • first: ์ฒซ ํŽ˜์ด์ง€์ธ์ง€?(boolean)
  • last: ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€์ธ์ง€?(boolean)

 

๏นกSpring Data์—์„œ ํŽ˜์ด์ง• ๋ฐ ์ •๋ ฌ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์†์‰ฝ๊ฒŒ ํŽ˜์ด์ง•, ์ •๋ ฌ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Œ

  • Pageable์€ ์†์‰ฝ๊ฒŒ ํŽ˜์ด์ง•, ์ •๋ ฌ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด ์ œ๊ณต๋˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ด๋ฉฐ, PageRequest๋Š” ํ•ด๋‹น ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌํ˜„์ฒด์ž„
  • ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ (ํ˜„์žฌ ํŽ˜์ด์ง€(0 ์‹œ์ž‘), ๋ฐ์ดํ„ฐ ๋…ธ์ถœ ๊ฐœ์ˆ˜, ์ •๋ ฌ ๋ฐฉ๋ฒ•(ASC, DESC))
  • ์ƒ์„ฑ๋œ Pageable ๊ตฌํ˜„ ๊ฐ์ฒด๋ฅผ Spring Data JPA์˜ Query Method ํŒŒ๋ผ๋ฏธํ„ฐ์— ํ•จ๊ป˜ ์ „๋‹ฌํ•˜๋ฉด ํŽ˜์ด์ง• ๋ฐ ์ •๋ ฌ ์ฒ˜๋ฆฌ๊ฐ€ ์™„๋ฃŒ๋œ ๋ฐ์ดํ„ฐ๋ฅผ Page ํƒ€์ž…์œผ๋กœ ๋ฐ˜ํ™˜ํ•จ
  • Page ํƒ€์ž…์—๋Š” Client์— ์ „๋‹ฌํ•ด์•ผํ•  ๋ฐ์ดํ„ฐ์ธ totalPages, totalElements ๋“ฑ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•จ๊ป˜ ํฌํ•จํ•˜๊ณ  ์žˆ์Œ

ํŽ˜์ด์ง• ๋ฐ ์ •๋ ฌ ๊ตฌํ˜„

public class ProductController {

    private final ProductService productService;

   	...

    @GetMapping("/products")
    public Page<ProductResponseDto> getProducts(
            @RequestParam("page") int page,
            @RequestParam("size") int size,
            @RequestParam("sortBy") String sortBy,
            @RequestParam("isAsc") boolean isAsc,
            @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return productService.getProducts(userDetails.getUser(),
                page-1, size, sortBy, isAsc);
    }
}
public class ProductService {

    private final ProductRepository productRepository;
    
    ...

    public Page<ProductResponseDto> getProducts(User user, int page, int size, String sortBy, boolean isAsc) {
        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, sortBy); // Sort ๊ฐ์ฒด = (direction ๋ฐฉํ–ฅ, ์ •๋ ฌ ํ•ญ๋ชฉ)
        Pageable pageable = PageRequest.of(page, size, sort);

        // ํ˜„์žฌ ๋“ค์–ด์˜จ User์˜ ๊ถŒํ•œ์ด Admin์ธ์ง€ User์ธ์ง€ ํ™•์ธ
        UserRoleEnum userRoleEnum = user.getRole();

        Page<Product> productList;

        if(userRoleEnum == UserRoleEnum.USER) {
            productList = productRepository.findAllByUser(user, pageable);
        } else {
            productList = productRepository.findAll(pageable);
        }

        return productList.map(ProductResponseDto::new);
    }
    ...
}

ํŽ˜์ด์ง•, ์ •๋ ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ฒฐ๊ณผ