backend
  • README
  • DOCS
    • Java Docs
    • Servlet Docs
    • JSP Docs
    • DB & SQL Docs
    • Spring Boot Docs
    • Spring Security Docs
    • AWS Docs
  • 설치하기
    • Intellij 설정
  • 자바
    • 01 Java란?
    • 02 자바 시작하기
    • 03 자료형과 연산자
    • 04 제어문
    • 05 메소드
    • 06 클래스 기초
      • Static 보충자료
      • 패키지 보충자료
    • 07 객체지향 프로그래밍
    • 08 클래스 더 알아보기
      • 열거형 ENUM 보충자료
    • 09 클래스와 자료형
      • 다형성 보충자료
      • 제네릭 보충자료
    • 10 컬렉션 프레임워크
      • 컬렉션 프레임워크 보충자료
    • 11 람다식과 함수형 프로그래밍
      • 람다식 보충자료
    • 12 오류 대비하기
      • 오류 보충자료
    • 13 멀티태스킹
      • 멀티태스킹 보충자료
    • 교재보충
      • java.lang
  • 스프링
    • 서블릿, JSP
      • 05 Servlet(서블릿)
        • 서블릿 보충자료
        • 서블릿 추가코드
        • XML, YAML, JSON
      • 06 JSP(자바 서버 페이지)
        • JSP 보충자료
      • 07 JSTL(JSP 스탠다드 태그 라이브러리)
        • JSTL 보충자료
      • 08 Cookie(쿠키), Session(세션)
      • 09 서블릿,필터,리스너
        • 서블릿,필터,리스너 보충자료
      • 11 도서관리 프로젝트 실습
    • Spring Boot
      • 01 스프링 등장 배경, 객체지향
        • 스프링 등장 배경, 객체지향 보충자료
      • 02 IOC(제어의 역전), DI(의존성 주입)
        • IOC 보충자료
        • DI 보충자료
      • 03 스프링 구조
        • 스프링 구조 보충설명
      • 04 테스트코드 실습
      • 05 스프링 빈 설정
        • 스프링 빈 설정 보충자료
      • 06 싱글톤
        • 싱글톤 보충 자료
      • 07 스프링 빈 자동설정
        • 스프링 빈 자동설정 보충자료
      • 08 빈 생명주기
        • 빈 생명주기 보충자료
      • 09 빈 스코프
        • 빈 스코프 보충자료
      • 10 스프링 MVC
        • 스프링 MVC 보충자료
        • 데이터베이스 연동에 필요한 부분
      • 11 Validation(검증)
        • Validation(검증) 보충자료
      • 12 Bean Validation(빈검증)
        • Bean Validation(빈검증) 보충자료
      • 13 예외처리
        • 예외처리 보충자료
      • 14 타입변환
      • 15 JDBC(Java Database Connectivity)
      • 16 커넥션풀
      • 17 트랜잭션
        • 트랜잭션 보충자료
      • 18 JDBC 템플릿 활용
      • 19 MyBatis
      • 20 JPA(Java Persistence API)
      • 22 게시판 프로젝트 실습
    • Spring Security
      • 보안(Security)
      • Spring Security
      • 2. Spring Security 알아보기
        • 보안 위협 실제 사례와 방어 전략
      • 3. Spring Security 기본 동작 흐름
      • 4. Spring Security로 인증 권한 추가하기
        • Spring Security의 인증 및 인가
      • 5. Spring Security에서 세션 관리하기
        • 세션(Session)과 쿠키(Cookie) 비교, 토큰(Token)과의 관계
        • 해싱 및 해싱알고리즘
        • base64
      • 6. Spring Security 악용 보호
        • SameSite
      • 7. Spring Security로 인가 권한 추가하기
      • 8. Bcrypt(비크립트) 암호화
      • OAuth2 적용하기
  • 네트워크
    • HTTP
    • OSI 7계층
  • DB&SQL
    • 01 Database(데이터베이스)와 SQL 개요
    • 02 관계형 모델
    • 03 집합
    • 04 JOIN 연산
    • 05 MySQL
      • 세이브포인트
      • DBeaver, Mysql 오토커밋 설정 관련
    • 06 SQL 기초
      • 예시데이터 쿼리문
    • 07 SQL 실습
      • 실습 스키마
    • 08 Join 활용
      • 실습스키마
    • 09 SQL 활용
      • 실습스키마
    • 10 정규화
      • 실습 스키마
    • 데이터타입
    • 예시 프로젝트 스키마 구성
  • AWS
    • SSL 연결하기
    • 보충설명
Powered by GitBook
On this page
  1. 스프링
  2. Spring Boot

10 스프링 MVC

p131 - build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.3'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'spring'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

p132(왼쪽이미지) - src/main/spring/mvc/domain/User

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long userIdx;
    private String userId;
    private String userName;
    private String rank;
    private LocalDate registrationDate;
    private LocalDate updateDate;
}

p132(오른쪽이미지) - src/main/spring/mvc/domain/Book

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Book {
    // 인덱스 번호
    private Long bookIdx;
    // 도서 이름
    private String title;
    // 출판사
    private String publisher;
    // 판매 가격
    private int salePrice;
    // 대여 비용
    private int rentalPrice;
    // 대여자
    private String renter;
    // 등록일자
    private LocalDate registrationDate;
    // 수정일자
    private LocalDate updateDate;
}

p134, 139 - src/main/spring/mvc/controller/UserController

@Slf4j
@Controller
public class UserController {

    @RequestMapping(value = "/user", method = RequestMethod.GET)
    public ModelAndView register() {
        ModelAndView modelAndView = new ModelAndView("register");
        modelAndView.addObject("user", new User()); // 빈 User 객체를 뷰에 전달
        return modelAndView;
    }

    @PostMapping("/submitUser")
    public ModelAndView submitUser(HttpServletRequest request) {
        // HttpServletRequest에서 폼 데이터를 직접 가져옴
        String userId = request.getParameter("userId");
        String userName = request.getParameter("userName");
        String rank = request.getParameter("rank");

        // User 객체를 수동으로 생성하고 값을 설정
        User user = new User();
        user.setUserId(userId);
        user.setUserName(userName);
        user.setRank(rank);
        user.setRegistrationDate(LocalDate.now());
        user.setUpdateDate(LocalDate.now());

        // ModelAndView 객체 생성 및 데이터와 뷰 설정
        ModelAndView modelAndView = new ModelAndView("userDetails");  // "userDetails"는 이동할 뷰 이름
        modelAndView.addObject("user", user);  // 모델에 user 객체 추가

        return modelAndView;  // ModelAndView 반환
    }

    @PostMapping("/submitUser")
    public String submitUser(
            @RequestParam("userId") String userId,
            @RequestParam("userName") String userName,
            @RequestParam("rank") String rank,
            Model model
    ) {
        // User 객체를 수동으로 생성하고 값을 설정
        User user = new User();
        user.setUserId(userId);
        user.setUserName(userName);
        user.setRank(rank);
        user.setRegistrationDate(LocalDate.now());
        user.setUpdateDate(LocalDate.now());

        model.addAttribute("user", user);  // 모델에 user 객체 추가

        return "userDetails";  // ModelAndView 반환
    }


}

p135(왼쪽이미지) - src/main/resources/templates/register.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>사용자 등록</title>
</head>
<body>
<h2>사용자 등록 폼</h2>
<form action="/submitUser" method="post">
  <label for="userId">아이디:</label>
  <input type="text" id="userId" name="userId" required>
  <br>

  <label for="userName">이름:</label>
  <input type="text" id="userName" name="userName" required>
  <br>

  <label for="rank">등급:</label>
  <input type="text" id="rank" name="rank" required>
  <br>

  <button type="submit">등록</button>
</form>
</body>
</html>

p135(오른쪽이미지) - src/main/resources/templates/userDetails.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>사용자 정보</title>
</head>
<body>
<h2>등록된 사용자 정보</h2>
<p>아이디: <span th:text="${user.userId}"></span></p>
<p>이름: <span th:text="${user.userName}"></span></p>
<p>등급: <span th:text="${user.rank}"></span></p>
<p>등록 날짜: <span th:text="${user.registrationDate}"></span></p>
<p>업데이트 날짜: <span th:text="${user.updateDate}"></span></p>
</body>
</html>

p142, 144 - src/main/java/spring/mvc/controller/LoggerController

@Slf4j
@RestController
public class LoggerController {
    @GetMapping("/log")
    public String getLogTest() {
        String name = "userName";
        System.out.println("일반 System.out.println");
        // Trace 로그 (가장 상세한 로그, 디버깅용)
        log.trace("Trace 레벨 로그: 이 메시지는 매우 상세한 정보를 포함합니다.");

        // Debug 로그 (디버깅 관련 정보)
        log.debug("Debug 레벨 로그: 디버깅을 위한 로그입니다.");

        // Info 로그 (일반적인 정보)
        log.info("Info 레벨 로그: 애플리케이션의 일반적인 동작을 나타냅니다. name={}",name);

        // Warn 로그 (경고 메시지, 오류까지는 아니지만 주의가 필요한 상황)
        log.warn("Warn 레벨 로그: 경고입니다. 잠재적인 문제가 있을 수 있습니다.");

        // Error 로그 (오류가 발생했을 때 사용)
        log.error("Error 레벨 로그: 오류가 발생했습니다!");

        return "로그 테스트 완료";
    }

//    private static final Logger logger = LoggerFactory.getLogger(LoggerController.class);

//    @GetMapping("/log")
//    public String getLogTest() {
//        System.out.println("일반 System.out.println");
//        // Trace 로그 (가장 상세한 로그, 디버깅용)
//        logger.trace("Trace 레벨 로그: 이 메시지는 매우 상세한 정보를 포함합니다.");
//
//        // Debug 로그 (디버깅 관련 정보)
//        logger.debug("Debug 레벨 로그: 디버깅을 위한 로그입니다.");
//
//        // Info 로그 (일반적인 정보)
//        logger.info("Info 레벨 로그: 애플리케이션의 일반적인 동작을 나타냅니다.");
//
//        // Warn 로그 (경고 메시지, 오류까지는 아니지만 주의가 필요한 상황)
//        logger.warn("Warn 레벨 로그: 경고입니다. 잠재적인 문제가 있을 수 있습니다.");
//
//        // Error 로그 (오류가 발생했을 때 사용)
//        logger.error("Error 레벨 로그: 오류가 발생했습니다!");
//
//        return "로그 테스트 완료";
//    }

}

p148,149,150,151,152,153,154 - src/main/java/spring/mvc/controller/VariableTestController

@Slf4j
@RestController
public class VariableTestController {

    @GetMapping("/path/{userId}")
    public boolean pathTest(
        @PathVariable String userId
    ) {
        log.info("path variable = {}", userId);
        return true;
    }

    @GetMapping("/path2")
    public boolean paramTest(
        @RequestParam(name = "name", defaultValue = "test", required = false) String name
    ) {
        log.info("defaultValue Test = {}", name);
        return true;
    }

    @GetMapping("/path3")
    public boolean paramMapTest(
        @RequestParam Map<String, Object> paramMap
    ) {
        String userName = (String) paramMap.get("userName");
        String userId = (String) paramMap.get("userId");

        log.info("userName = {}, userId = {}, age = {}", userName, userId);
        return true;
    }

    @GetMapping("/path4")
    public boolean modelAttributeTest(
        @ModelAttribute User user
    ) {
        log.info("userId = {}, userName = {}, userIdx = {}", user.getUserId(), user.getUserName(), user.getUserIdx());
        return true;
    }

    @PostMapping("/path5")
    public boolean requestBody(
            @RequestBody String message
    ) {
        log.info("message body = {}", message);
        return true;
    }

    @GetMapping(value = "/header", headers = "Special=specialTest")
    public boolean headerTest() {
        log.info("Special Test");
        return true;
    }

    @GetMapping(value = "/produces-consumes", consumes = "application/json", produces = "application/json")
    public String consumesProducesTest() {
        log.info("Consumes JSON and Produces JSON");
        return "{\"message\":\"Success\"}";
    }
}

p157, 158 - src/main/java/spring/mvc/repository/UserRepository

@Slf4j
@Repository
public class UserRepository {
    // ConcurrentHashMap을 이용한 데이터 저장소
    private final ConcurrentMap<Long, User> usersMap = new ConcurrentHashMap<>();
    private static Long userIndex = 1L;


    // 기본 유저 등록
    public UserRepository() {
    }

    // CREATE: User 객체 저장
    public User save(User user) {
        user.setUserIdx(++userIndex);
        usersMap.put(user.getUserIdx(), user);
        return user;
    }

    // READ: userIdx로 검색
    public Optional<User> findByUserIdx(Long userIdx) {
        return Optional.ofNullable(usersMap.get(userIdx));
    }

    // READ: user_id로 검색
    public Optional<User> findByUserId(String user_id) {
        return usersMap.values().stream()
                .filter(user -> user.getUserId().equals(user_id))
                .findAny();
    }

    // READ: user_name으로 검색
    public List<User> findByUserName(String user_name) {
        return usersMap.values().stream()
                .filter(user -> user.getUserName().equals(user_name))
                .collect(Collectors.toList());
    }

    // READ: 모든 데이터 찾기
    public List<User> findAll() {
        return new ArrayList<>(usersMap.values());
    }

    // UPDATE: 특정 User 업데이트
    public Optional<User> update(Long userIdx, User updatedUser) {
        if (usersMap.containsKey(userIdx)) {
            updatedUser.setUserIdx(userIdx); // 기존 userIdx 유지
            usersMap.put(userIdx, updatedUser);
            return Optional.of(updatedUser);
        }
        return Optional.empty();
    }

    // DELETE: 특정 User 삭제
    public boolean delete(Long userIdx) {
        return usersMap.remove(userIdx) != null;
    }
}

p159~163 - src/test/java/spring/mvc/repository/UserRepositoryTest

class UserRepositoryTest {

    private UserRepository userRepository;

    @BeforeEach
    void setUp() {
        userRepository = new UserRepository();
    }

    @Test
    void save() {
        // given
        User user = new User(null, "user1", "User One", "A", null, null);

        // when
        User savedUser = userRepository.save(user);

        // then
        assertNotNull(savedUser.getUserIdx());
        Assertions.assertThat(userRepository.findByUserIdx(savedUser.getUserIdx())).isPresent();
        assertEquals("user1", savedUser.getUserId());
    }

    @Test
    void findByUserIdx() {
        // given
        User user = new User(null, "user2", "User Two", "B", null, null);
        User savedUser = userRepository.save(user);

        // when
        Optional<User> foundUser = userRepository.findByUserIdx(savedUser.getUserIdx());

        // then
        assertTrue(foundUser.isPresent());
        assertEquals(savedUser.getUserIdx(), foundUser.get().getUserIdx());
        assertEquals("User Two", foundUser.get().getUserName());
    }

    @Test
    void findByUserId() {
        // given
        User user = new User(null, "user3", "User Three", "A", null, null);
        userRepository.save(user);

        // when
        Optional<User> foundUser = userRepository.findByUserId("user3");

        // then
        assertTrue(foundUser.isPresent());
        assertEquals("User Three", foundUser.get().getUserName());
    }

    @Test
    void findByUserName() {
        // given
        User user1 = new User(null, "user4", "Same Name", "A", null, null);
        User user2 = new User(null, "user5", "Same Name", "B", null, null);
        userRepository.save(user1);
        userRepository.save(user2);

        // when
        List<User> foundUsers = userRepository.findByUserName("Same Name");

        // then
        assertEquals(2, foundUsers.size());
        assertTrue(foundUsers.stream().allMatch(user -> user.getUserName().equals("Same Name")));
    }

    @Test
    void findAll() {
        // given
        User user1 = new User(null, "user6", "User Six", "A", null, null);
        User user2 = new User(null, "user7", "User Seven", "B", null, null);
        userRepository.save(user1);
        userRepository.save(user2);

        // when
        List<User> allUsers = userRepository.findAll();

        // then
        assertEquals(2, allUsers.size());
    }

    @Test
    void update() {
        // given
        User user = new User(null, "user8", "User Eight", "A", null, null);
        User savedUser = userRepository.save(user);

        // when
        savedUser.setUserName("Updated User Eight");
        savedUser.setRank("B");
        Optional<User> updatedUser = userRepository.update(savedUser.getUserIdx(), savedUser);

        // then
        assertTrue(updatedUser.isPresent());
        assertEquals("Updated User Eight", updatedUser.get().getUserName());
        assertEquals("B", updatedUser.get().getRank());
    }

    @Test
    void delete() {
        // given
        User user = new User(null, "user9", "User Nine", "A", null, null);
        User savedUser = userRepository.save(user);

        // when
        boolean isDeleted = userRepository.delete(savedUser.getUserIdx());

        // then
        assertTrue(isDeleted);
        assertFalse(userRepository.findByUserIdx(savedUser.getUserIdx()).isPresent());
    }
}

p167,168 - src/main/java/spring/mvc/service/UserService

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // CREATE: 새로운 사용자 생성
    public User createUser(User user) {
        return userRepository.save(user);
    }

    // READ: userIdx로 사용자 검색
    public Optional<User> getUserById(Long userIdx) {
        return userRepository.findByUserIdx(userIdx);
    }

    // READ: userId로 사용자 검색
    public Optional<User> getUserByUserId(String userId) {
        return userRepository.findByUserId(userId);
    }

    // READ: userName으로 사용자 검색
    public List<User> getUsersByUserName(String userName) {
        return userRepository.findByUserName(userName);
    }

    // READ: 모든 사용자 조회
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    // UPDATE: 기존 사용자 정보 업데이트
    public Optional<User> updateUser(Long userIdx, User updatedUser) {
        return userRepository.update(userIdx, updatedUser);
    }

    // DELETE: 사용자 삭제
    public boolean deleteUser(Long userIdx) {
        return userRepository.delete(userIdx);
    }
}

p169~173 - src/main/java/spring/mvc/controller/UserRestController

@Slf4j
@RestController
@RequestMapping("/user")
public class UserRestController {

    private final ObjectMapper objectMapper = new ObjectMapper();
    private final UserService userService;

    @Autowired
    public UserRestController(UserService userService) {
        this.userService = userService;
    }

    // 에러 메시지를 JSON 형태로 반환하는 메서드
    private String getErrorJson(String message) {
        Map<String, String> errorResponse = new HashMap<>();
        errorResponse.put("error", message);
        try {
            return objectMapper.writeValueAsString(errorResponse);
        } catch (JsonProcessingException e) {
            return "{\"error\": \"JSON 처리 중 오류가 발생했습니다.\"}";
        }
    }

    /**
     * 등록 (POST) - 새로운 사용자 생성
     */
    @PostMapping("/")
    public String registerUser(@RequestBody User newUser) {
        try {
            User createdUser = userService.createUser(newUser);
            String returnJson = objectMapper.writeValueAsString(createdUser);
            log.info("User created: {}", returnJson);
            return returnJson;
        } catch (JsonProcessingException e) {
            log.error("JSON 처리 오류: {}", e.getMessage());
            return getErrorJson("사용자 생성 중 오류 발생");
        }
    }

    /**
     * 조회 (GET) - 모든 사용자 목록 조회
     */
    @GetMapping("/list")
    public String listUsers() {
        try {
            List<User> users = userService.getAllUsers();
            String returnJson = objectMapper.writeValueAsString(users);
            log.info("Users list: {}", returnJson);
            return returnJson;
        } catch (JsonProcessingException e) {
            log.error("JSON 처리 오류: {}", e.getMessage());
            return getErrorJson("사용자 목록 조회 중 오류 발생");
        }
    }

    /**
     * 조회 (GET) - ID로 사용자 조회
     */
    @GetMapping("/{userIdx}")
    public String getUserById(@PathVariable Long userIdx) {
        Optional<User> user = userService.getUserById(userIdx);
        if (user.isPresent()) {
            try {
                String returnJson = objectMapper.writeValueAsString(user.get());
                log.info("User found: {}", returnJson);
                return returnJson;
            } catch (JsonProcessingException e) {
                log.error("JSON 처리 오류: {}", e.getMessage());
                return getErrorJson("사용자 조회 중 오류 발생");
            }
        } else {
            log.warn("User with ID {} not found", userIdx);
            return getErrorJson("사용자를 찾을 수 없습니다.");
        }
    }

    /**
     * 수정 (PATCH) - ID로 사용자 정보 수정
     */
    @PatchMapping("/{userIdx}")
    public String updateUser(@PathVariable Long userIdx, @RequestBody User updatedUser) {
        Optional<User> updated = userService.updateUser(userIdx, updatedUser);
        if (updated.isPresent()) {
            try {
                String returnJson = objectMapper.writeValueAsString(updated.get());
                log.info("User updated: {}", returnJson);
                return returnJson;
            } catch (JsonProcessingException e) {
                log.error("JSON 처리 오류: {}", e.getMessage());
                return getErrorJson("사용자 업데이트 중 오류 발생");
            }
        } else {
            log.warn("User with ID {} not found", userIdx);
            return getErrorJson("사용자를 찾을 수 없습니다.");
        }
    }

    /**
     * 삭제 (DELETE) - ID로 사용자 삭제
     */
    @DeleteMapping("/{userIdx}")
    public String deleteUser(@PathVariable Long userIdx) {
        boolean isDeleted = userService.deleteUser(userIdx);
        if (isDeleted) {
            log.info("User with ID {} deleted successfully", userIdx);
            return "{\"message\": \"사용자가 성공적으로 삭제되었습니다.\"}";
        } else {
            log.warn("User with ID {} not found", userIdx);
            return getErrorJson("사용자를 찾을 수 없습니다.");
        }
    }
}

p174 - src/main/java/spring/mvc/domain/Book

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Book {
    // 인덱스 번호
    private Long bookIdx;
    // 도서 이름
    @NotBlank
    private String title;
    // 출판사
    @NotBlank
    private String publisher;
    // 판매 가격
    @NotNull
    @Min(0)
    private int salePrice;
    // 대여 비용
    private int rentalPrice;
    // 대여자
    private String renter;
    // 등록일자
    private LocalDate registrationDate;
    // 수정일자
    private LocalDate updateDate;
}

p175 - src/main/java/spring/mvc/repository/BookRepository

@Repository
public class BookRepository {
    private final ConcurrentMap<Long, Book> bookMap = new ConcurrentHashMap<>();
    private static Long bookIndex = 0L;

    // private 생성자: 외부에서 인스턴스 생성 불가
    private BookRepository() {}

    // CREATE: Book 객체 저장
    public Book save(Book book) {
        book.setBookIdx(++bookIndex);
        bookMap.put(book.getBookIdx(), book);
        return book;
    }

    // READ: ID로 책 검색
    public Optional<Book> findById(Long bookIdx) {
        return Optional.ofNullable(bookMap.get(bookIdx));
    }

    // READ: 모든 책 리스트 반환
    public List<Book> findAll() {
        return new ArrayList<>(bookMap.values());
    }

    // UPDATE: 기존 책 정보 수정
    public Optional<Book> update(Long bookIdx, Book updatedBook) {
        if (bookMap.containsKey(bookIdx)) {
            updatedBook.setBookIdx(bookIdx); // 기존 ID 유지
            bookMap.put(bookIdx, updatedBook);
            return Optional.of(updatedBook);
        }
        return Optional.empty();
    }

    // DELETE: ID로 책 삭제
    public boolean delete(Long bookIdx) {
        return bookMap.remove(bookIdx) != null;
    }
}

p177, 178 - src/main/java/spring/mvc/service/BookService

@Service
public class BookService {

    private final BookRepository bookRepository;
    private final UserRepository userRepository;

    @Autowired
    public BookService(BookRepository bookRepository, UserRepository userRepository) {
        this.bookRepository = bookRepository;
        this.userRepository = userRepository;
    }

    // CREATE: 새로운 책 저장
    public Book createBook(Book book) {
        return bookRepository.save(book);
    }

    // READ: ID로 책 검색
    public Optional<Book> getBookById(Long bookIdx) {
        return bookRepository.findById(bookIdx);
    }

    // READ: 모든 책 리스트 검색
    public List<Book> getAllBooks() {
        return bookRepository.findAll();
    }

    // UPDATE: 책 정보 수정
    public Optional<Book> updateBook(Long bookIdx, Book updatedBook) {
        return bookRepository.update(bookIdx, updatedBook);
    }

    // DELETE: 책 삭제
    public boolean deleteBook(Long bookIdx) {
        return bookRepository.delete(bookIdx);
    }

    // BOOKING: userIdx로 유저 검색해서 renter에 유저 이름 추가
    public Optional<Book> rentBook(Long bookIdx, Long userIdx) {
        // 책 검색
        Optional<Book> bookOpt = bookRepository.findById(bookIdx);
        if (bookOpt.isPresent()) {
            Book book = bookOpt.get();

            // 이미 대여 중인 경우 에러 처리
            if (book.getRenter() != null && !book.getRenter().isEmpty()) {
                throw new IllegalStateException("이 책은 이미 대여 중입니다.");
            }

            // 유저 검색
            Optional<User> userOpt = userRepository.findByUserIdx(userIdx);
            if (userOpt.isPresent()) {
                User user = userOpt.get();
                // 책의 대여자 정보를 유저 이름으로 설정
                book.setRenter(user.getUserName());
                // 업데이트된 책 정보 저장
                return bookRepository.update(bookIdx, book);
            } else {
                throw new IllegalArgumentException("유효하지 않은 사용자입니다.");
            }
        }
        return Optional.empty();
    }

    // RETURN: 책 반납
    public Optional<Book> returnBook(Long bookIdx, Long userIdx) {
        // 책 검색
        Optional<Book> bookOpt = bookRepository.findById(bookIdx);
        if (bookOpt.isPresent()) {
            Book book = bookOpt.get();

            // 책이 대여 중인지 확인
            if (book.getRenter() == null || book.getRenter().isEmpty()) {
                throw new IllegalStateException("이 책은 대여 중이 아닙니다.");
            }

            // 유저 검색
            Optional<User> userOpt = userRepository.findByUserIdx(userIdx);
            if (userOpt.isPresent()) {
                User user = userOpt.get();

                // 반납하는 유저가 대여자와 일치하는지 확인
                if (!book.getRenter().equals(user.getUserName())) {
                    throw new IllegalArgumentException("반납 요청자가 대여자와 일치하지 않습니다.");
                }

                // 책의 대여자 정보를 삭제 (반납 처리)
                book.setRenter(null);
                // 업데이트된 책 정보 저장
                return bookRepository.update(bookIdx, book);
            } else {
                throw new IllegalArgumentException("유효하지 않은 사용자입니다.");
            }
        }
        return Optional.empty();
    }
}

p179 ~186- src/test/java/spring/mvc/service/BookServiceTest

class BookServiceTest {

    @Mock
    private BookRepository bookRepository;

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private BookService bookService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void createBook() {
        Book book = new Book();
        book.setTitle("Test Book");

        when(bookRepository.save(book)).thenReturn(book);

        Book createdBook = bookService.createBook(book);

        assertNotNull(createdBook);
        assertEquals("Test Book", createdBook.getTitle());
        verify(bookRepository, times(1)).save(book);
    }

    @Test
    void getBookById() {
        Long bookIdx = 1L;
        Book book = new Book();
        book.setBookIdx(bookIdx);
        book.setTitle("Test Book");

        when(bookRepository.findById(bookIdx)).thenReturn(Optional.of(book));

        Optional<Book> foundBook = bookService.getBookById(bookIdx);

        assertTrue(foundBook.isPresent());
        assertEquals(bookIdx, foundBook.get().getBookIdx());
        verify(bookRepository, times(1)).findById(bookIdx);
    }

    @Test
    void getAllBooks() {
        Book book1 = new Book();
        book1.setTitle("Book 1");
        Book book2 = new Book();
        book2.setTitle("Book 2");

        when(bookRepository.findAll()).thenReturn(Arrays.asList(book1, book2));

        List<Book> books = bookService.getAllBooks();

        assertEquals(2, books.size());
        verify(bookRepository, times(1)).findAll();
    }

    @Test
    void updateBook() {
        Long bookIdx = 1L;
        Book updatedBook = new Book();
        updatedBook.setTitle("Updated Book");

        when(bookRepository.update(bookIdx, updatedBook)).thenReturn(Optional.of(updatedBook));

        Optional<Book> result = bookService.updateBook(bookIdx, updatedBook);

        assertTrue(result.isPresent());
        assertEquals("Updated Book", result.get().getTitle());
        verify(bookRepository, times(1)).update(bookIdx, updatedBook);
    }

    @Test
    void deleteBook() {
        Long bookIdx = 1L;

        when(bookRepository.delete(bookIdx)).thenReturn(true);

        boolean result = bookService.deleteBook(bookIdx);

        assertTrue(result);
        verify(bookRepository, times(1)).delete(bookIdx);
    }

    @Test
    void rentBook() {
        Long bookIdx = 1L;
        Long userIdx = 1L;

        Book book = new Book();
        book.setBookIdx(bookIdx);

        User user = new User();
        user.setUserIdx(userIdx);
        user.setUserName("Test User");

        // 레포지토리 동작 모킹
        when(bookRepository.findById(bookIdx)).thenReturn(Optional.of(book));
        when(userRepository.findByUserIdx(userIdx)).thenReturn(Optional.of(user));
        when(bookRepository.update(bookIdx, book)).thenReturn(Optional.of(book));

        // rentBook 메서드 호출
        Optional<Book> rentedBook = bookService.rentBook(bookIdx, userIdx);

        // 대여된 책이 존재하는지 검증
        assertTrue(rentedBook.isPresent());
        assertEquals("Test User", rentedBook.get().getRenter());

        // 레포지토리 메서드가 올바르게 호출되었는지 검증
        verify(bookRepository, times(1)).findById(bookIdx);
        verify(userRepository, times(1)).findByUserIdx(userIdx);
        verify(bookRepository, times(1)).update(bookIdx, book);
    }

    @Test
    void rentBookAlreadyRented() {
        Long bookIdx = 1L;
        Long userIdx = 1L;

        Book book = new Book();
        book.setBookIdx(bookIdx);
        book.setRenter("Already Rented");

        // 레포지토리 동작 모킹
        when(bookRepository.findById(bookIdx)).thenReturn(Optional.of(book));

        // 예외가 발생하는지 검증
        IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
            bookService.rentBook(bookIdx, userIdx);
        });

        // 예외 메시지가 올바른지 검증
        assertEquals("이 책은 이미 대여 중입니다.", exception.getMessage());

        // 책이 검색되었는지 확인, 다른 작업은 수행되지 않음
        verify(bookRepository, times(1)).findById(bookIdx);
        verify(userRepository, times(0)).findByUserIdx(anyLong());  // 유저 확인은 호출되지 않아야 함
        verify(bookRepository, times(0)).update(anyLong(), any(Book.class));
    }

    @Test
    void rentBookUserNotFound() {
        Long bookIdx = 1L;
        Long userIdx = 1L;

        Book book = new Book();
        book.setBookIdx(bookIdx);

        // 레포지토리 동작 모킹
        when(bookRepository.findById(bookIdx)).thenReturn(Optional.of(book));
        when(userRepository.findByUserIdx(userIdx)).thenReturn(Optional.empty());

        // 예외가 발생하는지 검증
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
            bookService.rentBook(bookIdx, userIdx);
        });

        // 예외 메시지가 올바른지 검증
        assertEquals("유효하지 않은 사용자입니다.", exception.getMessage());

        // 레포지토리 메서드가 올바르게 호출되었는지 검증
        verify(bookRepository, times(1)).findById(bookIdx);
        verify(userRepository, times(1)).findByUserIdx(userIdx);
        verify(bookRepository, times(0)).update(anyLong(), any(Book.class));
    }

    @Test
    void rentBookBookNotFound() {
        Long bookIdx = 1L;
        Long userIdx = 1L;

        // 레포지토리 동작 모킹
        when(bookRepository.findById(bookIdx)).thenReturn(Optional.empty());

        // rentBook 메서드 호출
        Optional<Book> rentedBook = bookService.rentBook(bookIdx, userIdx);

        // 대여된 책이 없음을 검증
        assertFalse(rentedBook.isPresent());

        // 레포지토리 메서드가 올바르게 호출되었는지 검증
        verify(bookRepository, times(1)).findById(bookIdx);
        verify(userRepository, times(0)).findByUserIdx(anyLong());
        verify(bookRepository, times(0)).update(anyLong(), any(Book.class));
    }

    @Test
    void returnBook() {
        Long bookIdx = 1L;
        Long userIdx = 1L;
        Book book = new Book();
        book.setBookIdx(bookIdx);
        book.setRenter("Test User");
        User user = new User();
        user.setUserIdx(userIdx);
        user.setUserName("Test User");

        when(bookRepository.findById(bookIdx)).thenReturn(Optional.of(book));
        when(userRepository.findByUserIdx(userIdx)).thenReturn(Optional.of(user));
        when(bookRepository.update(bookIdx, book)).thenReturn(Optional.of(book));

        Optional<Book> returnedBook = bookService.returnBook(bookIdx, userIdx);

        assertTrue(returnedBook.isPresent());
        assertNull(returnedBook.get().getRenter());
        verify(bookRepository, times(1)).findById(bookIdx);
        verify(userRepository, times(1)).findByUserIdx(userIdx);
        verify(bookRepository, times(1)).update(bookIdx, book);
    }

    @Test
    void returnBookUserNotFound() {
        Long bookIdx = 1L;
        Long userIdx = 1L;

        Book book = new Book();
        book.setBookIdx(bookIdx);
        book.setRenter("Test User");

        // 책은 존재하지만 유저는 존재하지 않는 경우를 모킹
        when(bookRepository.findById(bookIdx)).thenReturn(Optional.of(book));
        when(userRepository.findByUserIdx(userIdx)).thenReturn(Optional.empty());

        // 예외가 발생하는지 검증
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
            bookService.returnBook(bookIdx, userIdx);
        });

        // 예외 메시지가 올바른지 검증
        assertEquals("유효하지 않은 사용자입니다.", exception.getMessage());

        // 레포지토리 메서드 호출 검증
        verify(bookRepository, times(1)).findById(bookIdx);
        verify(userRepository, times(1)).findByUserIdx(userIdx);
        verify(bookRepository, times(0)).update(anyLong(), any(Book.class));
    }

    @Test
    void returnBookWrongUser() {
        Long bookIdx = 1L;
        Long userIdx = 1L;

        Book book = new Book();
        book.setBookIdx(bookIdx);
        book.setRenter("Test User");

        User user = new User();
        user.setUserIdx(userIdx);
        user.setUserName("Another User");

        // 책과 유저를 모킹, 유저가 대여자와 다름
        when(bookRepository.findById(bookIdx)).thenReturn(Optional.of(book));
        when(userRepository.findByUserIdx(userIdx)).thenReturn(Optional.of(user));

        // 예외가 발생하는지 검증
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
            bookService.returnBook(bookIdx, userIdx);
        });

        // 예외 메시지가 올바른지 검증
        assertEquals("반납 요청자가 대여자와 일치하지 않습니다.", exception.getMessage());

        // 레포지토리 메서드 호출 검증
        verify(bookRepository, times(1)).findById(bookIdx);
        verify(userRepository, times(1)).findByUserIdx(userIdx);
        verify(bookRepository, times(0)).update(anyLong(), any(Book.class));
    }

    @Test
    void returnBookBookNotFound() {
        Long bookIdx = 1L;
        Long userIdx = 1L;

        // 책이 존재하지 않는 경우를 모킹
        when(bookRepository.findById(bookIdx)).thenReturn(Optional.empty());

        // 메서드 호출
        Optional<Book> returnedBook = bookService.returnBook(bookIdx, userIdx);

        // 반환 값이 비어있는지 확인
        assertFalse(returnedBook.isPresent());

        // 레포지토리 메서드 호출 검증
        verify(bookRepository, times(1)).findById(bookIdx);
        verify(userRepository, times(0)).findByUserIdx(anyLong());
        verify(bookRepository, times(0)).update(anyLong(), any(Book.class));
    }

    @Test
    void returnBookNotRented() {
        Long bookIdx = 1L;
        Long userIdx = 1L;
        Book book = new Book();
        book.setBookIdx(bookIdx);

        when(bookRepository.findById(bookIdx)).thenReturn(Optional.of(book));

        IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
            bookService.returnBook(bookIdx, userIdx);
        });

        assertEquals("이 책은 대여 중이 아닙니다.", exception.getMessage());
        verify(bookRepository, times(1)).findById(bookIdx);
    }
}

p187 ~ 193 - src/main/java/spring/mvc/controller/BookController

@Slf4j
@RestController
@RequestMapping("/books")
public class BookController {

    private final BookService bookService;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    // JSON 변환을 위한 헬퍼 메서드
    private String convertToJson(Object object) {
        try {
            return objectMapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            log.error("JSON 변환 오류: {}", e.getMessage());
            return "{\"error\": \"JSON 변환 중 오류가 발생했습니다.\"}";
        }
    }

    // CREATE: 새로운 책 저장
    @PostMapping
    public String createBook(@RequestBody Book book) {
        Book createdBook = bookService.createBook(book);
        return convertToJson(createdBook);
    }

    // READ: ID로 책 검색
    @GetMapping("/{bookIdx}")
    public String getBookById(@PathVariable Long bookIdx) {
        Optional<Book> book = bookService.getBookById(bookIdx);
        if (book.isPresent()) {
            return convertToJson(book.get());
        } else {
            return "{\"error\": \"책을 찾을 수 없습니다.\"}";
        }
    }

    // READ: 모든 책 리스트 검색
    @GetMapping
    public String getAllBooks() {
        List<Book> books = bookService.getAllBooks();
        return convertToJson(books);
    }

    // UPDATE: 책 정보 수정
    @PutMapping("/{bookIdx}")
    public String updateBook(@PathVariable Long bookIdx, @RequestBody Book updatedBook) {
        Optional<Book> book = bookService.updateBook(bookIdx, updatedBook);
        if (book.isPresent()) {
            return convertToJson(book.get());
        } else {
            return "{\"error\": \"책을 찾을 수 없습니다.\"}";
        }
    }

    // DELETE: 책 삭제
    @DeleteMapping("/{bookIdx}")
    public String deleteBook(@PathVariable Long bookIdx) {
        boolean deleted = bookService.deleteBook(bookIdx);
        if (deleted) {
            return "{\"message\": \"책이 성공적으로 삭제되었습니다.\"}";
        } else {
            return "{\"error\": \"책을 찾을 수 없습니다.\"}";
        }
    }

    // BOOKING: 책 대여
    @PostMapping("/{bookIdx}/rent/{userIdx}")
    public String rentBook(@PathVariable Long bookIdx, @PathVariable Long userIdx) {
        try {
            Optional<Book> rentedBook = bookService.rentBook(bookIdx, userIdx);
            if (rentedBook.isPresent()) {
                return convertToJson(rentedBook.get());
            } else {
                return "{\"error\": \"책을 찾을 수 없습니다.\"}";
            }
        } catch (IllegalStateException | IllegalArgumentException e) {
            log.error("책 대여 오류: {}", e.getMessage());
            return "{\"error\": \"" + e.getMessage() + "\"}";
        }
    }

    // RETURN: 책 반납
    @PostMapping("/{bookIdx}/return/{userIdx}")
    public String returnBook(@PathVariable Long bookIdx, @PathVariable Long userIdx) {
        try {
            Optional<Book> returnedBook = bookService.returnBook(bookIdx, userIdx);
            if (returnedBook.isPresent()) {
                return convertToJson(returnedBook.get());
            } else {
                return "{\"error\": \"책을 찾을 수 없습니다.\"}";
            }
        } catch (IllegalStateException | IllegalArgumentException e) {
            log.error("책 반납 오류: {}", e.getMessage());
            return "{\"error\": \"" + e.getMessage() + "\"}";
        }
    }
}

Previous빈 스코프 보충자료Next스프링 MVC 보충자료

Last updated 6 months ago