스프링이 뭔지도 모르는 이의 엉망진창 개발 일지입니다.
읽지 않는 것을 추천드리오나, 고통에 몸부림치는 뉴비의 일기장이 궁금하다면 말리지 않습니다.
2025-03-23
1:57
프로그램을 짤 때, 일단 컨트롤러부터 만든다고 한다.
그래서 controller 패키지에 있던 ReservationController를 열었다.
가장 먼저, 필요한 어노테이션을 작성하라는 문구가 보였다.
일단 컨트롤러니까 컨트롤러라고 명시했다.
@Controller
메서드에서 HTML 파일을 반환할 때 사용한다더라.
지금 내가 갖고 있는 `index.html`이나 `revervation_form.html` 말하는 것 같다.
만약에 JSON을 반환해야 한다면 @RestController를 써야 한다더라.
물론 이번 프로젝트는 웹 페이지를 렌더링하는 역할이므로 @Controller가 적합하다.
근데 채찍피티한테 물어보니까 어노테이션을 하나 더 추가하라고 하더라.
@RequestMapping("/reservations")
얘기를 들어보니 컨트롤러 전체에 공통적인 경로를 부여하는 거란다.
저걸 추가함으로써 이 컨트롤러에 있는 모든 요청의 기본 경로가 `/reservations`로 시작하게 된다.
@RequestMapping("/reservations")를 안 썼다면, 각 메서드의 URL을 개별적으로 설정해야 한다.
@GetMapping("/reservations") // 예약 목록 조회
@GetMapping("/reservations/new") // 예약 페이지
@PostMapping("/reservations") // 예약 생성
@PostMapping("/reservations/delete/{id}") // 예약 취소
지금 어차피 `ReservationController`니까 `/reservations`로 한 거고, 만약에 UserController나 OrderController 같은 거였으면 `/users`나 `/orders`였을 것 같다.


아무튼 어떤 느낌이냐면, 추후 공통 경로를 한 번에 관리할 수도 있으니까, 마치 변수처럼 유지보수하기 쉽게 만드는 뭐 그런 거 같다.
2:26
보니까.. 메서드마다 Mapping이라는 걸 하는 것 같다.
이걸 안 하면 스프링이 어떤 HTTP 요청을 처리해야 할지 모른다고 한다.
그렇다면 URL을 입력해도 작동을 안 하겠지.
HTTP 주요 메서드, 공부했잖아!
GET, POST, PUT, DELETE, PATCH, 으이?
적절하게 달아주자.
@GetMapping
public String getReservations(Model model) {
// TODO : 예약 메인 페이지를 가져오는 코드를 작성해주세요.
return null;
}
@GetMapping("/new")
public String showReservationForm() {
// TODO : 예약하기 페이지를 가져오는 코드를 작성해주세요.
return null;
}

일단 이 새끼들.. 대놓고 페이지를 가져오는 코드란다.
바로 @GetMapping 붙여버렸다.
@PostMapping("/new")
public String createReservation(@RequestParam Long doctorId, @RequestParam Long patientId) {
// TODO : 예약을 진행하는 코드를 작성해주세요.
return null;
}
@RequestParam
요청 파라미터 (doctorId, patientId) 값을 받아오는 어노테이션
사용자가 예약할 때 doctorId와 patientId를 입력폼에서 전송하면, @RequestParam을 통해 해당 값을 컨트롤러 메서드의 매개변수로 받는다. 만약 없다면 폼을 제출해도 의사랑 환자 아이디가 전달되지 않는다.
@PostMapping("/delete/{id}")
public String cancelReservation(@PathVariable Long id) {
// TODO : 예약을 취소하는 코드를 작성해주세요.
return null;
}
@PathVariable
URL 경로 변수 (/reservations/delete/{id}) 값을 받아오는 어노테이션
예약 취소 시, 특정 예약 ID를 삭제해야 한다. 예를 들어서 `POST /reservations/delete/5` 요청이 들어오면 `id = 5` 값을 받아서 삭제할 수 있도록 한다. 만약 없다면 URL에 포함된 id 값을 컨트롤러가 받을 수 없을 것이고 어떤 예약을 취소해야 하는지 길을 잃는다.
그리고 이 새끼들.. 메서드 이름부터가 생성이고 취소다.
새 예약을 생성하려면 폼을 제출해야 하니까, 폼 데이터를 전송할 때 쓰는 POST.
사용자가 예약을 취소하려면 버튼을 눌러야 하니까 POST. (솔직히 왜인지 모르겠다.)
templates 자세히 보니까 대놓고 method="post"라고 적혀있더라.
<form th:action="@{/reservations}" method="post">
<label for="doctorId">의사 ID:</label>
<input type="number" id="doctorId" name="doctorId" required>
<br>
<label for="patientId">환자 ID:</label>
<input type="number" id="patientId" name="patientId" required>
<br>
<button type="submit">예약하기</button>
</form>
<tr th:each="reservation : ${reservations}">
<td th:text="${reservation.id}"></td>
<td th:text="${reservation.doctorId}"></td>
<td th:text="${reservation.patientId}"></td>
<td th:text="${reservation.reservationTime}"></td>
<td>
<form th:action="@{/reservations/delete/{id}(id=${reservation.id})}" method="post">
<button type="submit">취소</button>
</form>
</td>
</tr>
templates 뒤져서 `요청 경로`라던가, `메서드` 꼭 체크하자.
2:52
주입 받아야 할 객체는 뭘까.
컨트롤러의 역할을 요청을 받아서 처리하고 적절한 응답을 반환하는 것이다.
지금 짜고 있는 컨트롤러는 예약 컨트롤러니까, 예약을 관리하는 서비스가 필요하다.
컨트롤러의 각 기능을 수행하기 위해 필요한 객체를 떠올리자.
예약을 생성하고 조회하고 삭제하는 `ReservationService`가 있어야 하고, DB에서 예약 정보를 가져오거나 저장한다면 ReservationRepository가 필요할 것이다.
근데 컨트롤러에서 직접 DB에 접근하면 MVC 구조가 깨질테니까, 서비스 레이어를 통해 DB 작업을 위임할 거다. 그러니까 `ReservationService`만 주입 받자.
물론 JPA를 사용하면 `ReservationRepository`는 단순히 `JpaRepository`를 상속받아서 기본적인 `CRUD` 기능을 제공하는 역할을 하게 된다. 따라서, CRUD 메서드들이 자동으로 제공되기 때문에 `ReservationService`에서 명시적으로 DB 작업을 수행할 필요 없이, repository를 통해 간단히 데이터를 조작할 수 있다.
예를 들어, save(), findById(), deleteById() 같은 메서드들 사용할 수 있다.
public interface ReservationRepository extends JpaRepository<Reservation, Long> {
// 기본적인 CRUD는 JpaRepository가 제공한다.
// 필요하다면 추가적인 쿼리 메서드를 정의할 수 있다.
}
개발자가 직접 DB 쿼리를 작성할 필요가 없다. 이러한 편의성은 JPA가 내부적으로 SQL을 생성하고 실행해 주기 때문이다.
@Controller
@RequestMapping("/reservations")
public class ReservationController {
private final ReservationService reservationService;
public ReservationController(ReservationService reservationService) {
this.reservationService = reservationService;
}
}
스프링에서는 자동으로 의존성 주입을 해 주며, 주로 `생성자 주입` 방식을 사용한다.
객체가 생성될 때 의존 객체를 반드시 주입 받게 되기 때문에, 불변성을 보장하고 테스트 용이성이 높다.
`필드 주입`과 `세터 주입`도 있다만, 스프링에서 권장하는 건 `생성자 주입`이다.
3:12
배가 고파서 설향 딸기 한 팩과 누텔라 와플을 주입했다.
3:25
메서드 바디 만들자.
@GetMapping
public String getReservations(Model model) {
model.addAttribute("reservations", reservationService.getAllReservations());
return "index";
}
서비스에 이미 메서드 뼈대는 다 있더라.
`ReservationService에서 모든 예약 목록을 가져온 후 Model에 담아서 뷰에 전달한다.
이후, index.html 템플릿을 반환하여 예약 목록을 화면에 표시한다.
@Service
public class ReservationService {
private final ReservationRepository reservationRepository;
public ReservationService(ReservationRepository reservationRepository) {
this.reservationRepository = reservationRepository;
}
}
컨트롤러와 마찬가지로 서비스도 생성자 주입해 주었다.
리포지토리에서 가져와야 하니까.
public List<Reservation> getAllReservations() {
return reservationRepository.findAll();
}
private final List<Reservation> reservations = new ArrayList<>();
private Long nextId = 1L;
public List<Reservation> findAll() {
return reservations;
}
5:12
하다 보니까 감잡아서 기록 안 하고 쭉쭉 만들었다.
이제야 조금 알 것 같다. (아마도)
</table>
<br>
<a href="/reservations/new">새 예약 추가</a>
</body>
</html>
예를 들어 위 코드는 `새 예약 추가` 버튼을 누르면 `/reservations/new` 경로로 간다는 의미다.
@GetMapping("/new")
public String showReservationForm() {
return "reservation_form";
}
그 경로는 @GetMapping 되어 있는 컨트롤러의 showReservationForm 메서드에 따라 `reservation_form.html`을 반환한다.
<tr th:each="reservation : ${reservations}">
<td th:text="${reservation.id}"></td>
<td th:text="${reservation.doctorId}"></td>
<td th:text="${reservation.patientId}"></td>
<td th:text="${reservation.reservationTime}"></td>
<td>
<form th:action="@{/reservations/delete/{id}(id=${reservation.id})}" method="post">
<button type="submit">취소</button>
</form>
</td>
</tr>
향상된 for문으로 순회하는 것 같다.
필드에 어떤 것들이 있어야 하는지도 다 박혀 있다.
`취소` 버튼 눌렀을 때 POST 요청을 보낸다는 정보와 경로 정보까지 포함하고 있다.
그리고 추측하건데, `reservation.*` 이 녀석들이 Getter를 요구하는 것 같다. (게터 안 쓰니까 에러가 발생했다.)
`Thymeleaf`로 작성했다는 게 뭔 소린가 했는데 아마 이거랑 관련이 있는 듯하다.
public void deleteById(Long id) {
reservations.removeIf(reservation -> reservation.getId().equals(id));
return;
}
다른 건 모르겠고 기록할 만한 건 람다식 포함된 메서드인 듯.
솔직히 뼈대 다 주고 시작한 거라 크게 어려운 과제는 아니었다.
사실 막연하게 백엔트 트랙을 지원했는데, 이번 과제를 진행하며 백엔드 일원으로서 역할을 하기 위해선 프론트가 작성한 템플릿을 이해하고, 요구사항에 맞춰 데이터를 적절히 처리하고 전달할 수 있어야 한다는 것을 느꼈다.
1차 리펙
무분별한 Setter 사용은 OOP의 핵심인 `정보 은닉`을 저해시킬 수 있다. 따라서 모든 Setter를 삭제하고 생성자를 추가하여 캡슐화를 보장했다.
public Reservation(Long id, Long doctorId, Long patientId, LocalDateTime reservationTime) {
this.id = id;
this.doctorId = doctorId;
this.patientId = patientId;
this.reservationTime = reservationTime;
}
public Reservation createReservation(Long doctorId, Long patientId) {
Reservation reservation = new Reservation(null, doctorId, patientId, LocalDateTime.now());
return reservationRepository.save(reservation);
}
public Reservation save(Reservation reservation) {
Reservation savedReservation = new Reservation(nextId++, reservation.getDoctorId(), reservation.getPatientId(), reservation.getReservationTime());
reservations.add(savedReservation);
return savedReservation;
}
예약 ID가 리포지토리에 존재하기 때문에, 일단 서비스 계층에서는 `null`로 대체했으며, DB 저장 단계에서 ID를 추가하여 최종적으로는 `savedReservation`을 추가했다. (추가로, 생성자가 생겼기 때문에 예약 필드 final 선언도 해 주었다.)
빌더를 쓰면 null을 넣는 과정이 없긴 할텐데 빌더는 빌더대로 장단점이 있는지라 일단 안 쓰기로 했다.
'멋쟁이사자처럼 > 개발 일기' 카테고리의 다른 글
고독한 개발 일기 #2 (0) | 2025.03.29 |
---|