Spring Boot & JPA Part2

남재상·2025년 3월 11일
post-thumbnail

Spring Boot & JPA to CodingApple

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

📅 작성일

2025년 3월 11일


📌 목차

  1. 소개

  2. part2-1 : 상품 추가기능 1 (Map 자료형)

  3. part2-2 : 상품 추가기능 2 & Navbar 만들기

  4. part2-3 : 상세페이지 만들기 1 (Optional)

  5. part2-4 : 상세페이지 만들기 2 & 예외처리

  6. part2-5 : REST API의 예외처리 방법

  7. part2-6 : Service 레이어로 분리하려면

  8. part2-7 : 수정기능 1

  9. part2-8 : 수정기능 2 (숙제)

  10. part2-9 : 삭제기능 1 (AJAX, query string)

  11. part2-10 : 삭제기능 2 (AJAX 추가 내용)

  12. part2-11 : Session, JWT, OAuth 개념 설명

  13. part2-12 : Spring Security 설치와 셋팅, Hashing

  14. part2-13 : 가입기능 만들기

  15. part2-14 : 로그인 기능 1

  16. part2-15 : 로그인 기능 2

  17. part2-16 : 로그인 기능 3 (유저정보 커스터마이징, CSRF)

  18. 참고 자료


📝 소개

Part 2 : Part 2


part2-1

🔹 Map

  • 오브젝트 같은 느낌임
// 만드는방법
Map<String, String> test = new HashMap<>();
test.put("name", "kim");

🔹 데이터 받기

<!-- wrtie.html -->
<form action="/add" method="POST">
  <input name="title" />
  <input name="price" />
  <button type="submit">버튼</button>
</form>
// controller
@PostMapping("/add")
    String addPost (@RequestParam Map formData) {
        System.out.println(formData.get("title"));
        System.out.println(formData.get("price"));
        return "redirect:/list";
    }

or

@PostMapping("/add")
    String addPost (String title, String price) {
        System.out.println(title);
        System.out.println(price);
        return "redirect:/list";
    }

part2-2

🔹 값 DB에 넣기

  • 롬복으로 Getter Setter만드는건 의미가없기때문에 그냥 직접 만드는게 좋다
@PostMapping("/add")
    String addPost (String title, Integer price) {
        Item item = new Item();
        item.setTitle(title);
        item.setPrice(price);
        itemRepository.save(item);
        return "write.html";
    }

or

@PostMapping("/add")
    String addPost (@ModelAttribute Item item) {
        itemRepository.save(item);
        return "redirect:/list";
    }

🔹 fragment

  • 타임리프 문법
  • 별도의 파일을 import해서 사용가능
<!-- nav.html -->
<div class="nav" th:fragment="navbar">
  <a class="logo">SpringMall</a>
  <a>List</a>
  <a>Write</a>
</div>

<!-- write.html -->
<div th:replace="~{ nav.html::navbar }"></div>

part2-3

🔹 Optional

  • null일 수도 있고 아닐 수도 있다는 타입
@GetMapping("/detail/{id}")
    String detail () {
        Optional<Item> result = itemRepository.findById(1L);
        return "detail.html";
    }

part2-4

🔹 상세페이지 만들기

@GetMapping("/detail/{id}")
    String detail (@PathVariable Long id, Model model) {
        Optional<Item> result = itemRepository.findById(id);
        if (result.isPresent()) {
            model.addAttribute("items", result.get());
        }
        else {
            return "redirect:/list";
        }
        return "detail.html";
    }

🔹 a링크를 타임리프로 만드는법

<a th:href="@{'/detail/' + ${items.id}}">링크</a>

part2-5

🔹 REST API의 예외처리

  • 개꿀팁 ajax로 서버통신하면 서버에서 redirect불가능
  • 에러나면 status를 보내줘야함
ResponseEntity<String > detail (@PathVariable Long id, Model model) {
    try {

    }
    catch (Exception e) {
        return ResponseEntity.status(400).body("니잘못임");
    }
}

🔹 status

  • HttpStatus 쓰면편함
ResponseEntity.status(HttpStatus.NOT_FOUND).body("니잘못임");

🔹 퍼블릭 에러파일

  • 에러나는 상황을 만들어놓은 파일
  • 다른 컨트롤러에서 throw new Exception(); 가 나오면 퍼블릭파일 호출
@ControllerAdvice
public class MyExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handler(){
        return ResponseEntity.status(400).body("에러남!");
    }
}

part2-6

🔹 레이어로 분리

  • Controller는 보통 데이터나 html을 보내는 역할을 한다
  • Service는 검사 및 DB입출력을 할 때 사용한다
// Service.java
@Service
@RequiredArgsConstructor
public class ItemService {
    private final ItemRepository itemRepository;

    public void saveItem(String title, Integer price) {
        Item item = new Item();
        item.setTitle(title);
        item.setPrice(price);
        itemRepository.save(item);
    }
}

// Controller
private final ItemService itemService;
@PostMapping("/add")
    String addPost (String title, Integer price) {
        itemService.saveItem(title, price);
        return "redirect:/list";
    }

part2-7

🔹 지난시간에 배운것

  • 인풋에 값을 미리 추가해주려면 th:value
    @GetMapping("/edit/{id}")
    String edit (@PathVariable Long id, Model model) {
        Optional<Item> result = itemRepository.findById(id);
        if (result.isPresent()) {
            model.addAttribute("items", result.get());
        }
        else {
            return "redirect:/list";
        }
        return "edit.html";
    }

part2-8

🔹 지난시간에 배운것

  • 인풋에 값을 미리 추가해주려면 th:value
@PostMapping("/edit")
    String editPost (String title, Integer price, Long id) {
        Item item = new Item();
        item.setId(id);
        item.setTitle(title);
        item.setPrice(price);
        itemRepository.save(item);
        return "redirect:/list";
    }
<form action="/edit" method="POST">
  <input name="id" th:value="${items.id}" style="display:none;" />
  <input name="title" th:value="${items.title}" />
  <input name="price" th:value="${items.price}" />
  <button type="submit">버튼</button>
</form>

part2-9

🔹 AJAX

  • html의 form은 get post밖에 못하는데 얘는 다됨
document.querySelectorAll(".btn")[0].addEventListener("click", function () {
  fetch("/test1", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title: "kim", price: 123 }),
  });
});

or;

// 이건 Get
document.querySelectorAll(".btn")[0].addEventListener("click", function () {
  fetch("/test1?title=kim&price=123");
});
@PostMapping("/test1")
    String test1 (@RequestBody Map<String, Object> body) {
        System.out.println(body);
        return "redirect:/list";
    }

part2-10

🔹 Delete

  • form형태만 리다이렉트 되므로 서버에서 이렇게 보내줘야함
@DeleteMapping("/item")
    ResponseEntity<String> deleteItem(@RequestParam Long id) {
        itemRepository.deleteById(id);
        return ResponseEntity.status(200).body("삭제완료");
    }
<span
  th:onclick="fetch('/item?id=[[${items.id}]]', { method : 'DELETE' } )
                .then(r => r.text())
                .then(()=>{
                    location.reload();
                })
            "
  >🗑️s</span
>

part2-11

🔹 Session

  • Session id는 랜덤한 문자열
  • 유저의 Get / Post 요청마다 로그인 상태 체크
  • DB조회를 많이해서 부담이 될 수 있음

🔹 JWT

  • JSON Web Token방식
  • 로그인 날짜, 아이디, 유효기간 등을 암호화해서 보냄
  • Get / Post 요청마다 DB조회를 안해서 부담이 적음
  • 유저가 많으면 사용하기 좋음
  • 다른사람이 사용 or 상대 강제로그아웃 하기 힘듬

🔹 OAuth

  • 로그인했을 때 사용권한을 대여
  • 카카오로 로그인 이런거랑 비슷한 느낌

part2-12

🔹 Spring Security

  • Session id는 랜덤한 문자열
  • 처음 설정하면 로그인 페이지 뜨는데 터미널에 문자 뜨니까 그거 사용하면됨
  • FilterChain은 요청과 응답사이에 자동으로 실행해주고 싶은 코드를 담는곳
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE'
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // 다른사이트에서 api요청하는것 그게 csrf공격
        http.csrf((csrf -> csrf.disable()));
        http.authorizeHttpRequests((authorize) ->
                // /**은 아무 url이라는 뜻,  permitAll()은 항상 허용하겠다 라는뜻 즉 모든 url 로그인 검사를 끔
                authorize.requestMatchers("/**").permitAll()
        );
        return http.build();
    }
}

🔹 Hashing

  • 문자열을 암호화
  • 같은 걸 암호화하면 똑같은 암호화 문자가 나옴
new BCryptPasswordEncoder().encode("문자")

part2-13

🔹 숙제

  • 숙제임

🔹 dependency injection

  • 객체가 직접 다른 객체를 생성하는 게 아니라, 필요한 객체를 외부에서 넣어주는 방식
  • 남이 만든 클래스의 object 를 ID로 쓰고 싶으면 @Bean 사용
@Bean
    PasswordEncoder 함수() {
        return new BCryptPasswordEncoder();
    }

part2-14

🔹 로그인 세팅

  • html만들기
// 폼으로 로그인 한다라는 뜻
// SecurityConfig.java
http.formLogin((formLogin) -> formLogin.loginPage("/login")
                .defaultSuccessUrl("/")
                .failureUrl("/fail")
        )

// MyUserDetailsService.java
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        DB에서 username을 가진 유저를 찾아와서
//        return new User(유저아이디, 비번, 권한) 해주세요
    }
}

// interface파일
public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findAllByUsername(String username);
}
<!-- 에러가 난 경우 보여줌 -->
<div th:if="${param.error}">아이디 비번틀림</div>

part2-15

🔹 로그인한 사용자의 정보

  • 셋팅만 하면 세션방식 로그인 기능 구현 끝
  • 클래스에 가이드 주려면 interface/implements
  • Controller에서 유저 로그인 정보 출력가능
  • 타임리프 파일에서도 가능
// MyUserDetailsService.java
public class MyUserDetailsService implements UserDetailsService {
    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        DB에서 username을 가진 유저를 찾아와서
//        return new User(유저아이디, 비번, 권한) 해주세요
        var result = memberRepository.findAllByUsername(username);
        if(result.isEmpty()) {
            throw new UsernameNotFoundException("그런 아이디 없음");
        }
        var user = result.get();
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("일반유저"));
        return new User(user.getUsername(), user.getPassword(),authorities);
    }
}

// controller.java
public String myPage(Authentication auth) {
    System.out.println(auth);
    System.out.println(auth.getName()); //아이디출력가능
    System.out.println(auth.isAuthenticated()); //로그인여부 검사가능
    System.out.println(auth.getAuthorities().contains(new SimpleGrantedAuthority("일반유저")));
    return "mypage.html";
  }

// SecurityFilterChain
http.logout( logout -> logout.logoutUrl("/logout") );
<span sec:authentication="principal"></span>
<span sec:authentication="principal.username"></span>
<span sec:authentication="principal.authorities"></span>

part2-16

🔹 세션 유지기간

server.servlet.session.timeout=5s
server.servlet.session.cookie.max-age=5s

🔹 CSRF 토큰

// SecurityConfig
@Bean
  public CsrfTokenRepository csrfTokenRepository() {
    HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
    repository.setHeaderName("X-XSRF-TOKEN");
    return repository;
  }

  http.csrf(csrf -> csrf.csrfTokenRepository(csrfTokenRepository())
       .ignoringRequestMatchers("/login")
)

📚 참고 자료

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

0개의 댓글