๐Ÿ“ GitHub ๋ฐ”๋กœ๊ฐ€๊ธฐ

1 JWT ์„ค๊ณ„

1๏ธโƒฃ JWT dependency

compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

2๏ธโƒฃ SecretKey โ†’ application.properties

jwt.secret.key=7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZzrgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=

3๏ธโƒฃ JwtUtil.java

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {

    /* 1. JWT Token ์ƒ์„ฑ ํ•„์š”๊ฐ’ */
    // Header Key ๊ฐ’
    public static final String AUTHORIZATION_HEADER = "Authorization";

    // ์‚ฌ์šฉ์ž ๊ถŒํ•œ๊ฐ’ Key
    public static final String AUTHORIZATION_KEY = "auth";

    // JWT Token ์ธ์ฆ๋ฐฉ์‹ (Bearer + JWT Token ํ•จ๊ป˜ ์ „์†ก)
    private static final String BEARER_PREFIX = "Bearer ";

    // JWT Token ๋งŒ๋ฃŒ์‹œ๊ฐ„
    private static final long TOKEN_TIME = 60 * 60 * 1000L; // ms๊ธฐ์ค€ -> 1์‹œ๊ฐ„

    // application.properties -> JWT Secret Key ๋ถˆ๋Ÿฌ์˜ด
    @Value("${jwt.secret.key}")
    private String secretKey;

    // JWT Token ์ƒ์„ฑ ๋ฐ ๊ฒ€์ฆ Key ์ €์žฅ์šฉ๋„
    private Key key;

    // JWT Token ์ƒ์„ฑ(์‚ฌ์ธ)์•Œ๊ณ ๋ฆฌ์ฆ˜
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // Spring Bean ์ดˆ๊ธฐํ™” -> ์‹คํ–‰๋˜๋Š” ๋ฉ”์„œ๋“œ
    @PostConstruct
    public void init() {
        // secretKey -> ๋””์ฝ”๋”ฉ -> byte๋ฐฐ์—ด์— ๋‹ด์Œ
        byte[] bytes = Base64.getDecoder().decode(secretKey);

        // ๋””์ฝ”๋”ฉ ํ•œ byte๋ฐฐ์—ด -> JWT Token ์„œ๋ช… ๋ฐ ๊ฒ€์ฆ์— ํ•„์š”ํ•œ key ์ƒ์„ฑ
        key = Keys.hmacShaKeyFor(bytes);
    }


    /* 2. Client Request Header -> JWT Token ๊ฐ€์ ธ์˜ค๊ธฐ */
    public String getToken(HttpServletRequest httpServletRequest) {

        // Header์— ์žˆ๋Š” JWT Token -> clientToken์— ๋‹ด์Œ
        String clientToken = httpServletRequest.getHeader(AUTHORIZATION_HEADER);

        // clientToken ์กด์žฌ + Bearer๋กœ ์‹œ์ž‘ -> JWT Token ๊ฐ’๋งŒ ์ถ”์ถœ
        if (StringUtils.hasText(clientToken) && clientToken.startsWith(BEARER_PREFIX)) {
            return clientToken.substring(7);
        }

        return null;

    }


    /* 3. JWT Token ์ƒ์„ฑ */
    public String createToken(String username, UserRoleEnum userRoleEnum) {

        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username)
                        .claim(AUTHORIZATION_KEY, userRoleEnum)
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        .setIssuedAt(date)
                        .signWith(key, signatureAlgorithm)
                        .compact();

    }


    /* 4. JWT Token ๊ฒ€์ฆ */
    public boolean validateToken(String token) {

        try {

            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;

        } catch (SecurityException | MalformedJwtException exception) {
            log.info("Invalid JWT signature, ์œ ํšจํ•˜์ง€ ์•Š๋Š” JWT ์„œ๋ช… ์ž…๋‹ˆ๋‹ค");
        } catch (ExpiredJwtException exception) {
            log.info("Expired JWT Token, ๋งŒ๋ฃŒ๋œ JWT Token ์ž…๋‹ˆ๋‹ค");
        } catch (UnsupportedJwtException exception) {
            log.info("Unsupported JWT Token, ์ง€์›๋˜์ง€ ์•Š๋Š” JWT Token ์ž…๋‹ˆ๋‹ค");
        } catch (IllegalArgumentException exception) {
            log.info("JWT claims is empty, ์ž˜๋ชป๋œ JWT Token ์ž…๋‹ˆ๋‹ค");
        }

        return false;

    }


    /* 5. JWT Token -> ํšŒ์›์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ */
    public Claims getUserInfoFromToken(String token) {

        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();

    }

}
  • import org.springframework.beans.factory.annotation.Value; โ†’ @Value import๋ฌธ ์ˆ˜์ •์ ์šฉ

2 JWT์ ์šฉ ์„ค๊ณ„

๋กœ๊ทธ์ธ์„ฑ๊ณต โ†’ HTTP Response Header์— JWT Token ์ƒ์„ฑ ํ›„, ์‹ค์–ด Client์—๊ฒŒ ๋ณด๋ƒ„

1๏ธโƒฃ UserController.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	// ๋กœ๊ทธ์ธ
    @ResponseBody
    @PostMapping("/login")
    public String login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse httpServletResponse) {

        userService.login(loginRequestDto, httpServletResponse);

        return "success";

    }

2๏ธโƒฃ UserService.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */
// ๋กœ๊ทธ์ธ
    @Transactional(readOnly = true)
    public void login(LoginRequestDto loginRequestDto, HttpServletResponse httpServletResponse) {

        // 1. RequestDto -> ID/PW ๊ฐ€์ ธ์˜ด
        String username = loginRequestDto.getUsername();
        String password = loginRequestDto.getPassword();

        // 2. ํšŒ์›์œ ํšจ์„ฑ๊ฒ€์‚ฌ
        User user = userRepository.findByUsername(username).orElseThrow(
                () -> new IllegalArgumentException("๋“ฑ๋ก๋œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค")
        );

        // 3. ๋น„๋ฐ€๋ฒˆํ˜ธ์œ ํšจ์„ฑ๊ฒ€์‚ฌ
        if (! user.getPassword().equals(password)) {
            throw new IllegalArgumentException("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค");
        }

        // 4. ๋กœ๊ทธ์ธ์„ฑ๊ณต -> Http Response Header์— JWT Token ๋ณด๋‚ด๊ธฐ
        httpServletResponse.addHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(user.getUsername(), user.getRole()));

    }

3 JWT์ธ๊ฐ€ ์„ค๊ณ„

1๏ธโƒฃ ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ

๐Ÿ“ ProductController.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

    // ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ
    @GetMapping("/products")
    public List<ProductResponseDto> getProducts(HttpServletRequest httpServletRequest) {

        return productService.getProducts(httpServletRequest);

    }
    
/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

๐Ÿ“ ProductService.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	// ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ
    @Transactional(readOnly = true)
    public List<ProductResponseDto> getProducts(HttpServletRequest httpServletRequest) {

        // 1. HTTP Request Header -> JWT Token ๊ฐ€์ ธ์˜ค๊ธฐ
        String token = jwtUtil.getToken(httpServletRequest);
        Claims claims;

        // 2. JWT Token ์žˆ๋Š”๊ฒฝ์šฐ์—๋งŒ -> ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ ๊ฐ€๋Šฅ
        if (token != null) {

            // 2-1. JWT Token ๊ฒ€์ฆ
            if (jwtUtil.validateToken(token)) {

                // true -> Token์—์„œ ์‚ฌ์šฉ์ž์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
                claims = jwtUtil.getUserInfoFromToken(token);

            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 2-2. Token์—์„œ ๊ฐ€์ ธ์˜จ ์‚ฌ์šฉ์ž์ •๋ณด -> DB์กฐํšŒ
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
            );

            // 2-3. ์‚ฌ์šฉ์ž๊ถŒํ•œ ADMIN ; ์ „์ฒด์กฐํšŒ, USER ; ๋ณธ์ธ ๊ด€์‹ฌ์ƒํ’ˆ์กฐํšŒ
            UserRoleEnum userRoleEnum = user.getRole();
            System.out.println("role = " + userRoleEnum);

            List<ProductResponseDto> productResponseDtoList = new ArrayList<>();
            List<Product> productList;

            if (userRoleEnum == UserRoleEnum.USER) {
                productList = productRepository.findAllByUserId(user.getId());
            } else {
                productList = productRepository.findAll();
            }

            for (Product product : productList) {
                productResponseDtoList.add(ProductResponseDto.of(product));
            }

            return productResponseDtoList;

        } else {

            return null;

        }

    }

/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

2๏ธโƒฃ ๊ด€์‹ฌ์ƒํ’ˆ ๋“ฑ๋ก

๐Ÿ“ ProductController.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	// ๊ด€์‹ฌ์ƒํ’ˆ ๋“ฑ๋ก
    @PostMapping("/products")
    public ProductResponseDto createProduct(@RequestBody ProductRequestDto productRequestDto, HttpServletRequest httpServletRequest) {

        return productService.createProduct(productRequestDto, httpServletRequest);

    }

/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

๐Ÿ“ ProductService.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

// ๊ด€์‹ฌ์ƒํ’ˆ ๋“ฑ๋ก
    @Transactional
    public ProductResponseDto createProduct(ProductRequestDto productRequestDto, HttpServletRequest httpServletRequest) {

        // 1. HTTP Request Header -> JWT Token ๊ฐ€์ ธ์˜ค๊ธฐ
        String token = jwtUtil.getToken(httpServletRequest);
        Claims claims;

        // 2. JWT Token ์žˆ๋Š”๊ฒฝ์šฐ์—๋งŒ -> ๊ด€์‹ฌ์ƒํ’ˆ ๋“ฑ๋ก ๊ฐ€๋Šฅ
        if (token != null) {

            // 2-1. JWT Token ๊ฒ€์ฆ
            if (jwtUtil.validateToken(token)) {

                // true -> Token์—์„œ ์‚ฌ์šฉ์ž์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
                claims = jwtUtil.getUserInfoFromToken(token);

            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 2-2. Token์—์„œ ๊ฐ€์ ธ์˜จ ์‚ฌ์šฉ์ž์ •๋ณด -> DB์กฐํšŒ
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
            );

            // 2-3. ์š”์ฒญ๋ฐ›์€ DTO -> DB์— ์ €์žฅํ•  ๊ฐ์ฒด์ƒ์„ฑ
            Product product = productRepository.saveAndFlush(Product.of(productRequestDto, user.getId()));

            return ProductResponseDto.of(product);

        } else {

            return null;

        }

    }

/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

3๏ธโƒฃ ๊ด€์‹ฌ์ƒํ’ˆ ์ตœ์ €๊ฐ€ ๋“ฑ๋ก

๐Ÿ“ ProductController.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	@PutMapping("/products/{id}")
    public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto productMypriceRequestDto, HttpServletRequest httpServletRequest) {

        return productService.updateProduct(id, productMypriceRequestDto, httpServletRequest);

    }

/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

๐Ÿ“ ProductService.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	// ๊ด€์‹ฌ์ƒํ’ˆ ์ตœ์ €๊ฐ€ ๋“ฑ๋ก
    @Transactional
    public Long updateProduct(Long id, ProductMypriceRequestDto productMypriceRequestDto, HttpServletRequest httpServletRequest) {

        // 1. HTTP Request Header -> JWT Token ๊ฐ€์ ธ์˜ค๊ธฐ
        String token = jwtUtil.getToken(httpServletRequest);
        Claims claims;

        // 2. JWT Token ์žˆ๋Š”๊ฒฝ์šฐ์—๋งŒ -> ๊ด€์‹ฌ์ƒํ’ˆ ๋“ฑ๋ก ๊ฐ€๋Šฅ
        if (token != null) {

            // 2-1. JWT Token ๊ฒ€์ฆ
            if (jwtUtil.validateToken(token)) {

                // true -> Token์—์„œ ์‚ฌ์šฉ์ž์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
                claims = jwtUtil.getUserInfoFromToken(token);

            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 2-2. Token์—์„œ ๊ฐ€์ ธ์˜จ ์‚ฌ์šฉ์ž์ •๋ณด -> DB์กฐํšŒ
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
            );

            Product product = productRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
                    () -> new IllegalArgumentException("ํ•ด๋‹น์ƒํ’ˆ์€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
            );

            product.update(productMypriceRequestDto);

            return product.getId();

        } else {

            return null;

        }
    }

/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

4 ํŽ˜์ด์ง• ๋ฐ ์ •๋ ฌ

๊ตฌ๋ถ„์ข…๋ฅ˜
ํŽ˜์ด์ง•- page : ์กฐํšŒํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (1๋ถ€ํ„ฐ ์‹œ์ž‘)
- size : ํ•œ ํŽ˜์ด์ง€์— ๋ณด์—ฌ์ค„ ๋‚ด์šฉ ๊ฐฏ์ˆ˜ (10๊ฐœ๋กœ ๊ณ ์ •)
์ •๋ ฌ- sortBy (์ •๋ ฌํ•ญ๋ชฉ) : id, title, lprice
- isAsc (์˜ค๋ฆ„์ฐจ์ˆœ) : true โ†’ asc ์˜ค๋ฆ„์ฐจ์ˆœ, false โ†’ desc ๋‚ด๋ฆผ์ฐจ์ˆœ

1๏ธโƒฃ API ๋ช…์„ธ์„œ - Product ๋ณ€๊ฒฝ

๊ธฐ๋ŠฅMethodURLRequestResponse
๋ฉ”์ธํŽ˜์ด์ง€GET/api/shop-index.html
Query๋กœ ์ƒํ’ˆ๊ฒ€์ƒ‰, ์ƒํ’ˆ๊ฒ€์ƒ‰๊ฒฐ๊ณผ ๋ชฉ๋ก๋ฐ˜ํ™˜GET/api/search?query=๊ฒ€์ƒ‰์–ด-[
{
ย ย "title" : String,
ย ย "image" : String,
ย ย "link" : String,
ย ย "lprice" : int
},
โ€ขโ€ขโ€ข
]
๊ด€์‹ฌ์ƒํ’ˆ ๋“ฑ๋กPOST/api/productsHeader
Authorization : Bearer <JWT>

{
ย ย "title" : String,
ย ย "image" : String,
ย ย "link" : String,
ย ย "lprice" : int
}
{
ย ย "id" : Long,
ย ย "title" : String,
ย ย "image" : String,
ย ย "link" : String,
ย ย "lprice" : int,
ย ย "myprice" : int
}
๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒGET/api/products?sortBy=String&isAsc=boolean&size=int&page=intHeader
Authorization : Bearer <JWT>
Page<Product>
๊ด€์‹ฌ์ƒํ’ˆ ์ตœ์ €๊ฐ€ ๋“ฑ๋กPUT/api/products/{id}Header
Authorization : Bearer <JWT>

{
ย ย "myprice" : int
}
id

2๏ธโƒฃ ProductController.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	// ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ
    @GetMapping("/products")
    public Page<Product> getProducts(
            @RequestParam("page") int page, // ์กฐํšŒํ•  ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (1๋ถ€ํ„ฐ ์‹œ์ž‘)
            @RequestParam("size") int size, // ํ•œ ํŽ˜์ด์ง€์— ๋ณด์—ฌ์ค„ ๊ฐœ์ˆ˜
            @RequestParam("sortBy") String sortBy, // ์ •๋ ฌํ•ญ๋ชฉ
            @RequestParam("isAsc") boolean isAsc, // ์˜ค๋ฆ„์ฐจ์ˆœ(True), ๋‚ด๋ฆผ์ฐจ์ˆœ(False)
            HttpServletRequest httpServletRequest) {

        return productService.getProducts(httpServletRequest, page-1, size, sortBy, isAsc);

    }

/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

3๏ธโƒฃ ProductService.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	// ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ
    @Transactional(readOnly = true)
    public Page<Product> getProducts(HttpServletRequest httpServletRequest,
                                     int page, int size, String sortBy, boolean isAsc) {

        // ํŽ˜์ด์ง•
        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;

        Sort sort = Sort.by(direction, sortBy);

        Pageable pageable = PageRequest.of(page, size, sort);

        // 1. HTTP Request Header -> JWT Token ๊ฐ€์ ธ์˜ค๊ธฐ
        String token = jwtUtil.getToken(httpServletRequest);
        Claims claims;

        // 2. JWT Token ์žˆ๋Š”๊ฒฝ์šฐ์—๋งŒ -> ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ ๊ฐ€๋Šฅ
        if (token != null) {

            // 2-1. JWT Token ๊ฒ€์ฆ
            if (jwtUtil.validateToken(token)) {

                // true -> Token์—์„œ ์‚ฌ์šฉ์ž์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
                claims = jwtUtil.getUserInfoFromToken(token);

            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 2-2. Token์—์„œ ๊ฐ€์ ธ์˜จ ์‚ฌ์šฉ์ž์ •๋ณด -> DB์กฐํšŒ
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
            );

            // 2-3. ์‚ฌ์šฉ์ž๊ถŒํ•œ ADMIN ; ์ „์ฒด์กฐํšŒ, USER ; ๋ณธ์ธ ๊ด€์‹ฌ์ƒํ’ˆ์กฐํšŒ
            UserRoleEnum userRoleEnum = user.getRole();
            System.out.println("role = " + userRoleEnum);

            Page<Product> productPage;

            if (userRoleEnum == UserRoleEnum.USER) {
                productPage = productRepository.findAllByUserId(user.getId(), pageable);
            } else {
                productPage = productRepository.findAll(pageable);
            }

            return productPage;


        } else {

            return null;

        }

    }

/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

4๏ธโƒฃ ProductRepository.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

public interface ProductRepository extends JpaRepository<Product, Long> {

    Page<Product> findAllByUserId(Long userId, Pageable pageable);

    Optional<Product> findByIdAndUserId(Long id, Long userId);

    Page<Product> findAll(Pageable pageable);

}

/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

5 ํด๋”์„ค๊ณ„ ๋ฐ JPA ์—ฐ๊ด€๊ด€๊ณ„

1๏ธโƒฃ ์„œ๋น„์Šค์š”๊ตฌ์‚ฌํ•ญ

์š”๊ตฌ์‚ฌํ•ญ๊ธฐ๋Šฅ
ํด๋” ์ƒ์„ฑ- ํšŒ์›๋ณ„ ํด๋” ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
- ํด๋” ์ถ”๊ฐ€ ์‹œ, 1~N๊ฐœ ํ•œ๋ฒˆ์— ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
ํด๋”์— ๊ด€์‹ฌ์ƒํ’ˆ ์‚ฝ์ž…- ๊ด€์‹ฌ์ƒํ’ˆ์€ ํด๋” N๊ฐœ์— ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ์Œ
- ๊ด€์‹ฌ์ƒํ’ˆ์ด ๋“ฑ๋ก๋˜๋Š” ์‹œ์  โ†’ ์–ด๋Š ํด๋”์—๋„ ์ €์žฅ โŒ
ํด๋” ๋ณ„ ์กฐํšŒ- ํšŒ์›์€ ํด๋” ๋ณ„ ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ ๊ฐ€๋Šฅ
- ์ „์ฒด : ํด๋”์™€ ์ƒ๊ด€์—†์ด ํšŒ์›์ด ์ €์žฅํ•œ ์ „์ฒด ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ
- ํด๋”๋ช… : ํด๋” ๋ณ„ ์ €์žฅ๋œ ๊ด€์‹ฌ์ƒํ’ˆ๋งŒ ์กฐํšŒ ๊ฐ€๋Šฅ

2๏ธโƒฃ JPA ์—ฐ๊ด€๊ด€๊ณ„ ์„ค์ •

  • ํšŒ์› 1๋ช…์ด ์—ฌ๋Ÿฌ๊ฐœ์˜ ํด๋”๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Œ โ†’ @OneToMany
public class User {

	@OneToMany
    private List<Folder> folderList;

}

// ํšŒ์›์ด ๊ฐ€์ง„ ํด๋” ์กฐํšŒ
List<Folder> folderList = user.getFolderList();
  • ํด๋” ์—ฌ๋Ÿฌ๊ฐœ๋ฅผ ํšŒ์› 1๋ช…์ด ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Œ โ†’ @ManyToOne
public class Folder {
	
    @ManyToOne
    private User user;
    
}

// ํด๋” ์†Œ์œ  ํšŒ์› ์กฐํšŒ
folder.getUser();
  • JPA ์—ฐ๊ด€๊ด€๊ณ„ Column ์„ค์ •
@ManyToOne
@JoinColumn(name = "USER_ID", nullable = false)
private User user;

// name : ์™ธ๋ž˜ํ‚ค๋ช…
// nullable : ์™ธ๋ž˜ํ‚ค null ํ—ˆ์šฉ์—ฌ๋ถ€


6 ํด๋”์ƒ์„ฑ ๋ฐ ์กฐํšŒ ์„ค๊ณ„

1๏ธโƒฃ API ๋ช…์„ธ์„œ - ํด๋”์ƒ์„ฑ ๋ฐ ์กฐํšŒ

๊ธฐ๋ŠฅMethodURLRequestResponse
ํด๋” ์ƒ์„ฑPOSTapi/folders{
ย ย "folderNames" : [String, โ€ขโ€ขโ€ข]
}
[String, โ€ขโ€ขโ€ข]
ํด๋” ์กฐํšŒGETapi/user-folder-index.html
model ์ถ”๊ฐ€ โ†’ folders

2๏ธโƒฃ ํด๋”์ƒ์„ฑ ์„ค๊ณ„

User.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

@OneToMany
List<Folder> folders = new ArrayList<>();

/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

Folder.java

@Entity
@Getter
@NoArgsConstructor
public class Folder {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    private String name;

    @ManyToOne
    @JoinColumn(name = "USER_ID", nullable = false)
    private User user;

    // ์ƒ์„ฑ์ž
    public Folder(String name, User user) {

        this.name = name;
        this.user = user;

    }

}

FolderRequestDto.java

@Getter
public class FolderRequestDto {

    List<String> folderNames;

}

FolderController.java

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

    private final FolderService folderService;

    // ํด๋” ์ƒ์„ฑ
    @PostMapping("/folders")
    public List<Folder> addFolders(@RequestBody FolderRequestDto folderRequestDto, HttpServletRequest httpServletRequest) {

        List<String> folderNames = folderRequestDto.getFolderNames();

        return folderService.addFolders(folderNames, httpServletRequest);

    }
    
}

FolderService.java

@Service
@RequiredArgsConstructor
public class FolderService {

    private final FolderRepository folderRepository;
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;


    // ํด๋” ์ƒ์„ฑ
    @Transactional
    public List<Folder> addFolders(List<String> folderNames, HttpServletRequest httpServletRequest) {

		// Client Request -> Token ๊ฐ€์ ธ์˜ค๊ธฐ
        String token = jwtUtil.getToken(httpServletRequest);
        Claims claims;

        // Token ์œ ํšจ -> ํด๋”์ƒ์„ฑ ๊ฐ€๋Šฅ
        if (token != null) {

            // Token ๊ฒ€์ฆ
            if (jwtUtil.validateToken(token)) {
                // ํ† ํฐ -> ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // ํ† ํฐ์—์„œ ๊ฐ€์ ธ์˜จ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ DB ์กฐํšŒ
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
            );

            List<Folder> folderList = new ArrayList<>();

            for (String folderName : folderNames) {
            
                Folder folder = new Folder(folderName, user);
                folderList.add(folder);
                
            }

            return folderRepository.saveAll(folderList);
            
        } else {
        
            return null;
            
        }
    }

    // ํด๋” ์กฐํšŒ
    public List<Folder> getFolders(HttpServletRequest httpServletRequest) {

        String token = jwtUtil.getToken(httpServletRequest);
        Claims claims;

        if (token != null) {
            
            if (jwtUtil.validateToken(token)) {
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // ํ† ํฐ์—์„œ ๊ฐ€์ ธ์˜จ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ DB ์กฐํšŒ
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
            );

            return folderRepository.findAllByUser(user);

        } else {
            return null;
        }
    }

}

FolderRepository.java

public interface FolderRepository extends JpaRepository<Folder, Long> {

    List<Folder> findAllByUser(User user);
    
}

ShopController.java

@Controller
@RequiredArgsConstructor
@RequestMapping("/api")
public class ShopController {

    private final FolderService folderService;

    @GetMapping("/shop")
    public ModelAndView shop() {
        return new ModelAndView("index");
    }

    // ๋กœ๊ทธ์ธ ํ•œ ์œ ์ € -> ๋ฉ”์ธํŽ˜์ด์ง€ ์š”์ฒญ ์‹œ, ํด๋” ์ž๋™ ์กฐํšŒ
    @GetMapping("/user-folder")
    public String getUserInfo(Model model, HttpServletRequest httpServletRequest) {

        model.addAttribute("folders", folderService.getFolders(httpServletRequest));

        return "/index :: #fragment";
    }

}

7 ๊ด€์‹ฌ์ƒํ’ˆ ํด๋”์ถ”๊ฐ€๊ธฐ๋Šฅ ์„ค๊ณ„

1๏ธโƒฃ ์š”๊ตฌ์‚ฌํ•ญ

๐Ÿ“Œ Folder์™€ Product JPA ์—ฐ๊ด€๊ด€๊ณ„ โžก๏ธ M : N

  • ๊ด€์‹ฌ์ƒํ’ˆ โ†’ ํด๋” 0 ~ N๊ฐœ ์„ค์ •๊ฐ€๋Šฅ

  • ๊ด€์‹ฌ์ƒํ’ˆ ๋“ฑ๋ก์‹œ์  โ†’ ์–ด๋–ค ํด๋”์—๋„ ์ €์žฅ โŒ

  • ๊ด€์‹ฌ์ƒํ’ˆ โ†’ ๊ธฐ ์ƒ์„ฑ๋œ ํด๋”๋ฅผ ์„ ํƒํ•ด ์ถ”๊ฐ€๊ฐ€๋Šฅ

2๏ธโƒฃ API ๋ช…์„ธ์„œ - ๊ด€์‹ฌ์ƒํ’ˆ ํด๋”์ถ”๊ฐ€๊ธฐ๋Šฅ

๊ธฐ๋ŠฅMethodURLRequestResponse
ํด๋” ์ „์ฒด ์กฐํšŒGETapi/folders-[
{
ย ย "id" : int,
ย ย "name" : String,
ย ย "user" : {
ย ย ย ย "id" : int,
ย ย ย ย "username" : String,
ย ย ย ย "password" : String,
ย ย ย ย "email" : String,
ย ย ย ย  "role" : String,
ย ย ย ย  "folders" : []
ย ย  }
},
โ€ขโ€ขโ€ข
]
๊ด€์‹ฌ์ƒํ’ˆ์— ํด๋” ์ถ”๊ฐ€POSTapi/products/{productId}/folder{productId} : ๊ด€์‹ฌ์ƒํ’ˆ Id

[Form ํ˜•ํƒœ]
folderId : ์ถ”๊ฐ€ํ•  ํด๋” Id
ํด๋”๊ฐ€ ์ถ”๊ฐ€ ๋œ ๊ด€์‹ฌ์ƒํ’ˆ Id

3๏ธโƒฃ ํด๋” ์ „์ฒด ์กฐํšŒ

FolderController.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	// ํด๋” ์ „์ฒด ์กฐํšŒ
    @GetMapping("/folders")
    public List<Folder> getFolders(HttpServletRequest httpServletRequest) {
    
        return folderService.getFolders(httpServletRequest);
        
    }

/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

4๏ธโƒฃ ๊ด€์‹ฌ์ƒํ’ˆ์— ํด๋”์ถ”๊ฐ€

ProductController.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	// ๊ด€์‹ฌ์ƒํ’ˆ์— ํด๋”์ถ”๊ฐ€
    @PostMapping("products/{productId}/folder")
    public Long addFolder(@PathVariable Long productId, @RequestParam Long folderId, HttpServletRequest httpServletRequest) {

        Product product = productService.addFolder(productId, folderId, httpServletRequest);

        return product.getId();

    }

ProductService.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	// ๊ด€์‹ฌ์ƒํ’ˆ์— ํด๋”์ถ”๊ฐ€
    @Transactional
    public Product addFolder(Long productId, Long folderId, HttpServletRequest httpServletRequest) {

        String token = jwtUtil.getToken(httpServletRequest);
        Claims claims;

        if (token != null) {

            if (jwtUtil.validateToken(token)) {
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // ์‚ฌ์šฉ์ž ์œ ํšจ์„ฑ๊ฒ€์‚ฌ
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค")
            );

            // ์ƒํ’ˆ ์œ ํšจ์„ฑ๊ฒ€์‚ฌ
            Product product = productRepository.findById(productId)
                    .orElseThrow( () -> new NullPointerException("ํ•ด๋‹น ์ƒํ’ˆ ์•„์ด๋””๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค") );

            // ํด๋” ์œ ํšจ์„ฑ๊ฒ€์‚ฌ
            Folder folder = folderRepository.findById(folderId)
                    .orElseThrow( () -> new NullPointerException("ํ•ด๋‹น ํด๋” ์•„์ด๋””๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค") );

            // ์ƒํ’ˆ, ํด๋” -> ๊ฐ™์€ ํšŒ์› ์†Œ์œ ์ธ์ง€ ํ™•์ธ
            Long loginUserId = user.getId();

            if (! product.getUserId().equals(loginUserId) || ! folder.getUser().getId().equals(loginUserId)) {
                throw new IllegalArgumentException("ํšŒ์›๋‹˜์˜ ๊ด€์‹ฌ์ƒํ’ˆ์ด ์•„๋‹ˆ๊ฑฐ๋‚˜, ํšŒ์›๋‹˜์˜ ํด๋”๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค");
            }

            product.addFolder(folder);

        } else {

            return null;

        }

    }

Product.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	@ManyToMany
    private List<Folder> folderList = new ArrayList<>();
    
    /* ์ค‘๊ฐ„๋ถ€๋ถ„ ์ƒ๋žต */
    
    // ๊ด€์‹ฌ์ƒํ’ˆ์— ํด๋”์ถ”๊ฐ€
    public void addFolder(Folder folder) {
        this.folderList.add(folder);
    }
    
/* ๋’ท๋ถ€๋ถ„ ์ƒ๋žต */

8 ํด๋”๋ณ„ ๊ด€์‹ฌ์ƒํ’ˆ์กฐํšŒ ์„ค๊ณ„

1๏ธโƒฃ ์š”๊ตฌ์‚ฌํ•ญ

  • ํšŒ์›์€ ํด๋” ๋ณ„ ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ๊ฐ€๋Šฅ

    • 'ํด๋”๋ณ„' : ํด๋”๋ณ„ ์ €์žฅ๋œ ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ๊ฐ€๋Šฅ

    • '์ „์ฒด' : ํด๋”์™€ ์ƒ๊ด€์—†์ด ํšŒ์›์ด ์ €์žฅํ•œ ์ „์ฒด ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ๊ฐ€๋Šฅ

2๏ธโƒฃ API ๋ช…์„ธ์„œ - ํด๋”๋ณ„ ๊ด€์‹ฌ์ƒํ’ˆ์กฐํšŒ

๊ธฐ๋ŠฅMethodURLRequestResponse
ํด๋” ๋ณ„ ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒGETapi/folders/{folderId}/products{folderId} : ์กฐํšŒ๋ฅผ ์›ํ•˜๋Š” ํด๋” IdPage<Product>

FolderController.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	// ํด๋” ๋ณ„ ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ
    @GetMapping("/folders/{folderId}/products")
    public Page<Product> getProductsInFolder (@PathVariable Long folderId,
                                              @RequestParam int page,
                                              @RequestParam int size,
                                              @RequestParam String sortBy,
                                              @RequestParam boolean isAsc,
                                              HttpServletRequest httpServletRequest) {

        return folderService.getProductsInFolder(folderId, page-1, size, sortBy, isAsc, httpServletRequest);

    }

FolderService.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	@Transactional(readOnly = true)
    public Page<Product> getProductsInFolder(Long folderId, int page, int size, String sortBy, boolean isAsc, HttpServletRequest httpServletRequest) {

        // ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ
        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);

        // Request์—์„œ Token ๊ฐ€์ ธ์˜ค๊ธฐ
        String token = jwtUtil.getToken(httpServletRequest);
        Claims claims;

        // ํ† ํฐ์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ๊ด€์‹ฌ์ƒํ’ˆ ์กฐํšŒ ๊ฐ€๋Šฅ
        if (token != null) {
            // Token ๊ฒ€์ฆ
            if (jwtUtil.validateToken(token)) {
                // ํ† ํฐ์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // ํ† ํฐ์—์„œ ๊ฐ€์ ธ์˜จ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ DB ์กฐํšŒ
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
            );

            return productRepository.findAllByUserIdAndFolderList_Id(user.getId(), folderId, pageable);

        } else {
            return null;
        }

ProductRepository.java

public interface ProductRepository extends JpaRepository<Product, Long> {

	Page<Product> findAllByUserIdAndFolderList_Id(Long userId, Long folderId, Pageable);

}

9 ํด๋”๋ช… ์ค‘๋ณต Issue

1๏ธโƒฃ ์ค‘๋ณต๊ฒ€์‚ฌ ๋ฉ”์„œ๋“œ์ถ”๊ฐ€

FolderService.java

private boolean isExistFolderName(String folderName, List<Folder> existFolderList) {

        // ๊ธฐ์กด ํด๋” ๋ฆฌ์ŠคํŠธ์—์„œ folder name ์ด ์žˆ๋Š”์ง€ ๊ฒ€์‚ฌ
        for (Folder existFolder : existFolderList) {
        
            if (existFolder.getName().equals(folderName)) {
                return true;
            }
            
        }

        return false;
    }

2๏ธโƒฃ ํด๋”์ƒ์„ฑ ์‹œ, ์ค‘๋ณต๊ฒ€์‚ฌ ๋ฉ”์„œ๋“œ์‚ฌ์šฉ

FolderService.java

/* ํด๋”์ƒ์„ฑ addFolders ๋ฉ”์„œ๋“œ */

	// ํด๋”๋ช… ์ค‘๋ณต๊ฒ€์‚ฌ
    List<Folder> existFolderList = folderRepository.findAllByUserAndNameIn(user, folderNames);

	List<Folder> folderList = new ArrayList<>();

	for (String folderName : folderNames) {

		if (! isExistFolderName(folderName, existFolderList)) {

			Folder folder = new Folder(folderName, user);
            folderList.add(folder);

		}

	}

	return folderRepository.saveAll(folderList);

3๏ธโƒฃ FolderRepository.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	List<Folder> findAllByUserAndNameIn(User user, List<String> names);

10 ํด๋” ๋‚ด ๊ด€์‹ฌ์ƒํ’ˆ ์ค‘๋ณต์ƒ์„ฑ Issue

1๏ธโƒฃ ProductService.java

/* ๊ด€์‹ฌ์ƒํ’ˆ์— ํด๋”์ถ”๊ฐ€ addFolder ๋ฉ”์„œ๋“œ */

	// ์ค‘๋ณตํ™•์ธ
    Optional<Product> overlapFolder = productRepository.findbyIdAndFolderList_Id(product.getId(), folder.getId());
    
    if (overlapFolder.isPresent()) {
    	throw new IllegalArgumentException("์ค‘๋ณต๋œ ํด๋”์ž…๋‹ˆ๋‹ค");
    }
    
    product.addFolder(folder);

2๏ธโƒฃ ProductRepository.java

/* ์•ž๋ถ€๋ถ„ ์ƒ๋žต */

	Optional<Product> findByIdAndFolderList_Id(Long productId, Long folderId);

Outro

  • ๋กœ๊ทธ์ธ ํ•œ User๊ฐ€ ๋ฉ”์ธํŽ˜์ด์ง€ ์š”์ฒญ ์‹œ, ์œ ์ €์˜ ์ด๋ฆ„ ๋ฐ˜ํ™˜

  • ShopController.java

@GetMapping("/user-info")
    @ResponseBody
    public String getUserName(HttpServletRequest httpServletRequest) {
        String token = jwtUtil.getToken(httpServletRequest);
        Claims claims;

        if (token != null) {
            // Token ๊ฒ€์ฆ
            if (jwtUtil.validateToken(token)) {
                // ํ† ํฐ์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // ํ† ํฐ์—์„œ ๊ฐ€์ ธ์˜จ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ DB ์กฐํšŒ
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
            );

            return user.getUsername();
        }else {
            return "fail";
        }
    }
profile
๐ŸฑSunyeon-Jeong, mallang developer๐Ÿฐ

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