_

Always be tactful

멋쟁이사자처럼/개발 일기

고독한 개발 일기 #1

funczun 2025. 3. 25. 03:25
스프링이 뭔지도 모르는 이의 엉망진창 개발 일지입니다.
읽지 않는 것을 추천드리오나, 고통에 몸부림치는 뉴비의 일기장이 궁금하다면 말리지 않습니다.

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;
}
더보기
index.html: 예약하기 페이지

일단 이 새끼들.. 대놓고 페이지를 가져오는 코드란다.

바로 @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