Spring Boot & JPA Part3

남재상·2025년 4월 7일
post-thumbnail

Spring Boot & JPA to CodingApple

코딩애플 강의를 통해 배운 Spring Boot & JPA를 정리한 글입니다.

📅 작성일

2025년 3월 11일


📌 목차

  1. 소개
  2. part3-1 : object를 변환할 땐 DTO
  3. part3-2 : 페이지 나누기 (pagination)
  4. part3-3 : 이미지 업로드 기능 1 (S3 셋팅)
  5. part3-4 : 이미지 업로드 기능 2 (Presigned URL)
  6. part3-5 : AWS Elastic Beanstalk에 Spring boot 서버 배포
  7. part3-6 : 댓글기능 1 (정규화)
  8. part3-7 : 댓글기능 2
  9. part3-8 : 검색기능 1 (index)
  10. part3-9 : 삭제기능 1 (AJAX, query string)
  11. part3-10 : 주문기능 1 (제2정규화)
  12. part3-11 : 주문기능 2 (@ManyToOne)
  13. part3-12 : 주문기능 3 (JOIN FETCH)
  14. part3-13 : 주문기능 4 (@OneToMany)
  15. part3-14 : JWT 회원기능 1 (수동 로그인기능)
  16. part3-15 : JWT 회원기능 2
  17. part3-16 : JWT 회원기능 3 (Filter)
  18. part3-17 : Transaction 1 (DB조작 실패시 롤백하려면)
  19. part3-18 : Transaction 2 (unchecked Exception, integration test)
  20. 참고 자료

📝 소개

Part 3 : Part 3


part3-1

🔹 DTO만들기기

  • DTO 쓰면 타입체크가 쉬움
  • 재사용이 쉬움
  • 다른데도 사용가능
// Controller
@GetMapping("/user/1")
    @ResponseBody
    Member getUser() {
        var a = memberRepository.findById(1L);
        var result = a.get();
        var data = new MemberDto(result.getUsername(), result.getDisplayName());
        return result;
    }

// DTO
class MemberDto {
    public String username;
    public String displayName;

    public MemberDto(String a, String b) {
        this.username = a;
        this.displayName = b;
    }
}

part3-2

🔹 DTO만들기기

  • DTO 쓰면 타입체크가 쉬움
// Repository
package com.example.shop.item;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ItemRepository extends JpaRepository<Item, Long> {
    Page<Item> findPageBy(Pageable page);
}


// Controller
@GetMapping("/list/page/{page}")
String getListPage(Model model, @PathVariable Integer page) {
    Page<Item> result = itemRepository.findPageBy(PageRequest.of(page-1,5));
    model.addAttribute("items", result);
    return "list.html";
}

part3-3

🔹 AWS등록, Presigned URL 확인


part3-4

🔹 Presigned URL 등록 및 적용


part3-5

🔹 AWS Elastic Beanstalk 사용


part3-6

🔹 정규화


part3-7

🔹 간단함


part3-8

🔹 간단함

// Repository.interface
package com.example.shop.item;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ItemRepository extends JpaRepository<Item, Long> {
    Page<Item> findPageBy(Pageable page);
    List<Item> findAllByTitleContains(String title);
}


// Item.java
package com.example.shop.item;

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

@Entity
@ToString
@Setter
@Getter
@Table(indexes = @Index(columnList = "title", name = "작명"))
public class Item {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long id;

    @Column(nullable = false)
    private String title;
    private Integer price;
}


// Controller.java
    @PostMapping("/search")
    String postSearch(@RequestParam String searchText) {
        var result = itemRepository.findAllByTitleContains(searchText);
        System.out.println(result);
        return "redirect:/list";
    }

part3-9

🔹 full text index

  • 컬럼에 있는 문장들에서 단어들을 다 추출해준 다음에 그 단어들을 정렬해두는 식으로 동작
  • 영어는 ㄱㅊ은데 한국어 일본어 중국어는 n-gram parser사용해야함
-- 인덱스생성
CREATE FULLTEXT INDEX title_search
ON shop.item(title) WITH PARSER ngram;
// Controller
@PostMapping("/search")
    String postSearch(@RequestParam String searchText) {
        var result = itemRepository.rawQuery1(searchText);
        System.out.println(result);
        return "redirect:/list";
    }


// Repository.js
package com.example.shop.item;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface ItemRepository extends JpaRepository<Item, Long> {
    Page<Item> findPageBy(Pageable page);
    List<Item> findAllByTitleContains(String title);

    @Query(value = "select * from shop.item where match(title) against(?1)", nativeQuery = true)
    List<Item> rawQuery1(String text);
}

part3-10

🔹 주문

// Entity - 시간 알아서 채워줌
@CreationTimestamp
LocalDateTime created;

part3-11

🔹 주문

// Entity - 시간 알아서 채워줌
@CreationTimestamp
LocalDateTime created;


// Entity - 해당 컬럼과 연결된 테이블들을 가져온다
@ManyToOne
@JoinColumn(name="member_id", foreignKey = @ForeignKey())
private Member memberId;

part3-12

🔹 ManyToOne 단점

  • select문을 너무 많이 가져온다

🔹 ManyToOne 극복

  • JPQL사용
    @Query(value = "SELECT s FROM Sales s JOIN FETCH s.member")
    List<Sales> customFindAll();

part3-13

🔹 ManyToOne

  • 전부다 쓸거 아니면 LAZY
  • 전부다 쓸거면 fetch 쓰는게 좋음
// 항상 가져옴
@ManyToOne(fetch = FetchType.EAGER)

// 필요할떄만 가져옴
@ManyToOne(fetch = FetchType.LAZY)

🔹 OneToMany

  • 행들 자동출력
    @OneToMany(mappedBy = "member")
    List<Sales> sales = new ArrayList<>();

🔹 결론

  • 현재테이블 = OneToMany, 상대테이블 = ManyToOne
  • OneToMany는 별루 안씀
  • 대신 테이블 관계파악, 행 지울 때 관련된 행 자동삭제 가능

part3-14

🔹 JWT

// build
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
implementation 'io.jsonwebtoken:jjwt-gson:0.12.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.5'

// Controller
private final AuthenticationManagerBuilder authenticationManagerBuilder;

@PostMapping("/login/jwt")
    @ResponseBody
    String loginJWT(@RequestBody Map<String, String> data) {

        var authToken = new UsernamePasswordAuthenticationToken(data.get("username"), data.get("password"));
        var auth = authenticationManagerBuilder.getObject().authenticate(authToken);
        SecurityContextHolder.getContext().setAuthentication(auth);
        return "redirect:/list";
    }

// Util
package com.example.shop.member;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;

@Component
public class JwtUtil {

    static final SecretKey key =
            Keys.hmacShaKeyFor(Decoders.BASE64.decode(
                    "jwtpassword123jwtpassword123jwtpassword123jwtpassword123jwtpassword"
            ));

    // JWT 만들어주는 함수
    public static String createToken() {

        String jwt = Jwts.builder()
                .claim("username", "어쩌구")
                .claim("displayName", "저쩌구")
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + 10000)) //유효기간 10초
                .signWith(key)
                .compact();
        return jwt;
    }

    // JWT 까주는 함수
    public static Claims extractToken(String token) {
        Claims claims = Jwts.parser().verifyWith(key).build()
                .parseSignedClaims(token).getPayload();
        return claims;
    }

}
<form>
  <input name="username" id="username" />
  <input name="password" id="password" type="password" />
</form>
<button onclick="loginJWT()">전송</button>
<script>
  function loginJWT() {
    fetch("/login/jwt", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        username: document.querySelector("#username").value,
        password: document.querySelector("#password").value,
      }),
    })
      .then((r) => r.text())
      .then((r) => {
        console.log(r);
      });
  }
</script>

part3-15

🔹 JWT

package com.example.shop.member;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import org.springframework.security.core.Authentication;


import javax.crypto.SecretKey;
import java.util.Date;
import java.util.stream.Collectors;

@Component
public class JwtUtil {

    static final SecretKey key =
            Keys.hmacShaKeyFor(Decoders.BASE64.decode(
                    "jwtpassword123jwtpassword123jwtpassword123jwtpassword123jwtpassword"
            ));

    // JWT 만들어주는 함수
    public static String createToken(Authentication auth) {
        var user = (CustomUser) auth.getPrincipal();
        var authorities =auth.getAuthorities().stream().map(a-> a.getAuthority()).collect(Collectors.joining(","));

        String jwt = Jwts.builder()
                .claim("username", user.getUsername())
                .claim("displayName", user.displayName)
                .claim("authorities", authorities)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + 10000)) //유효기간 10초
                .signWith(key)
                .compact();
        return jwt;
    }

    // JWT 까주는 함수
    public static Claims extractToken(String token) {
        Claims claims = Jwts.parser().verifyWith(key).build()
                .parseSignedClaims(token).getPayload();
        return claims;
    }

}


@PostMapping("/login/jwt")
    @ResponseBody
    String loginJWT(@RequestBody Map<String, String> data, HttpServletResponse response) {

        var authToken = new UsernamePasswordAuthenticationToken(data.get("username"), data.get("password"));
        var auth = authenticationManagerBuilder.getObject().authenticate(authToken);
        SecurityContextHolder.getContext().setAuthentication(auth);

        var jwt = JwtUtil.createToken(SecurityContextHolder.getContext().getAuthentication());
        System.out.println(jwt);

        var cookie = new Cookie("jwt", "jwt");
        cookie.setMaxAge(10);
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        response.addCookie(cookie);

        return jwt;
    }

part3-16

🔹 너무 어렵고 복잡하네

package com.example.shop.member;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import org.springframework.security.core.Authentication;


import javax.crypto.SecretKey;
import java.util.Date;
import java.util.stream.Collectors;

@Component
public class JwtUtil {

    static final SecretKey key =
            Keys.hmacShaKeyFor(Decoders.BASE64.decode(
                    "jwtpassword123jwtpassword123jwtpassword123jwtpassword123jwtpassword"
            ));

    // JWT 만들어주는 함수
    public static String createToken(Authentication auth) {
        var user = (CustomUser) auth.getPrincipal();
        var authorities =auth.getAuthorities().stream().map(a-> a.getAuthority()).collect(Collectors.joining(","));

        String jwt = Jwts.builder()
                .claim("username", user.getUsername())
                .claim("displayName", user.displayName)
                .claim("authorities", authorities)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + 10000)) //유효기간 10초
                .signWith(key)
                .compact();
        return jwt;
    }

    // JWT 까주는 함수
    public static Claims extractToken(String token) {
        Claims claims = Jwts.parser().verifyWith(key).build()
                .parseSignedClaims(token).getPayload();
        return claims;
    }

}

part3-17

🔹 세팅


part3-18

🔹 @Transactional

  • unchecked Exception이 발생할 때만 롤백

📚 참고 자료

profile
작은 코드 하나에도 책임을 담는 개발자입니다!

0개의 댓글