이전에 이미 한 번 배운 내용이라 강의 수강하는 데에 시간이 별로 안 걸릴 것이라고 생각했는데
생각보다 시간이 많이 걸리고 있다.
지난 번 다짐한 것 처럼 강의를 수강하는 데에 키워드 위주로만 기술해 놓고 넘어가는 방식으로 강의를 수강하려고 했지만 그래도 개념과 코드 정리하는 것을 놓치고 싶지 않게 된다.
생각보다 막상 강의를 들었을 때야 기억이 나는 부분도 많고,
강의 내용을 정리하면서 머릿속에는 어떤 부분이 들어있는지 모르는 부분은 어떤 것인지 정리가 안되어 있었다.
게다가 지난 팀 과제 중에 코드의 오류를 고치면서 깊게 이해하는 것이 중요하다는 것을 알게 되었는데, 코드를 내 머리로 직접 작성하지 않고 이미 완성된 코드를 보고 따라 구현해 온 지금의 겉핥기 수준에 혹시라도 팀 과제를 하게 되었을 때 나 혼자 모르는 부분이 있어 팀에게 민폐를 끼칠까봐 불안하다는 것이 그 이유인 것 같다.
이제 강의에서 사전 캠프 기간동안 진행되는 CRUD 과제에 필요한 내용이 나온다.
일단 이 부분을 위주로 확실하게 이해하고 빠르게 과제를 해내야겠다.
Spring Boot는 기본 경로 요청이 들어오면 (http://localhost:8080/)
src > main > resources > static 에 만든 index.html을 반환해준다.
- GET API 사용해서 메모 목록 불러오기
- POST API 사용해서 메모 신규 생성하기
- PUT API 사용해서 메모 내용 변경하기
- DELETE API 사용해서 메모 삭제하기

메모 데이터가 필요하고, 요청과 응답에 사용할 각각의 DTO가 필요하고,
앞서 배운 PathVariable 방식으로 메모의 id를 전달해 해당 메모를 변경, 삭제하는 것 같다.
URL에 /api 가 중복되어 컨트롤러의 @RequestMapping 경로에 /api 를 추가해주는게 좋을 것 같다.
package com.sparta.memo.entity;
import com.sparta.memo.dto.MemoRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class Memo {
private Long id;
private String username;
private String contents;
public Memo(MemoRequestDto requestDto) {
this.username = requestDto.getUsername();
this.contents = requestDto.getContents();
}
}
데이터 전송 및 이동을 위해 생성되는 객체 (POJO - Plain Old Java Object)
Client에서 보내오는 데이터를 객체로 처리할 때, Client에 Java 클래스를 처리 후 반환할 때, 서버의 계층간의 이동에 사용한다.
통상적인 규칙으로, Request의 데이터를 처리할 때 사용되는 객체는 RequestDto, Response를 할 때 사용되는 객체는 ResponseDto라는 이름을 붙여 DTO 클래스를 만든다.
id값을 제외한 내용을 담는다.
import lombok.Getter;
@Getter
public class MemoRequestDto {
private String username;
private String contents;
}
import lombok.Getter;
@Getter
public class MemoResponseDto {
private Long id;
private String username;
private String contents;
}
@PostMapping("/memos")
public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// Memo Max ID Check
Long maxId = memoList.size() > 0 ? Collections.max(memoList.keySet()) + 1 : 1;
memo.setId(maxId);
// DB 저장
memoList.put(memo.getId(), memo);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
return memoResponseDto;
}
DB의 엔티티 불러오기
각각의 엔티티를 DTO로 변환
DTO List를 Client 에게 반환
@GetMapping("/memos")
public List<MemoResponseDto> getMemos() {
// Map To List
List<MemoResponseDto> responseList = memoList.values().stream()
.map(MemoResponseDto::new).toList();
return responseList;
}
// Memo class
public void update(MemoRequestDto requestDto) {
this.username = requestDto.getUsername();
this.contents = requestDto.getContents();
}
@PutMapping("/memos/{id}")
public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
// 해당 메모가 DB에 존재하는지 확인
if(memoList.containsKey(id)) {
// 해당 메모 가져오기
Memo memo = memoList.get(id);
// memo 수정
memo.update(requestDto);
return memo.getId();
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
@DeleteMapping("/memos/{id}")
public Long deleteMemo(@PathVariable Long id) {
// 해당 메모가 DB에 존재하는지 확인
if(memoList.containsKey(id)) {
// 해당 메모 삭제하기
memoList.remove(id);
return id;
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
간단한 프로젝트라도 클라이언트와 서버 사이에 오가는 데이터로는 무엇이 있는지 생각하면서 구현하는게 쉽지 않았다. 이를 정리해주는 API 문서가 더더욱 중요할 것 같다 생각된다.
추가로 DB없이 내부 객체로만 데이터를 관리하니까 생성할 엔티티의 id값을 계산하는 작업이 구현하는 것도 신기했다.
Java에서 관계형 DB에 접근하기 위한 표준 API이다.
DB별 차이는 JDBC 드라이버가 구현하며, 이를 통해 Java 코드 구조는 유지하면서 DB 종류 변경이 가능하도록 한다.
Spring이 제공하는 JDBC 추상화 도구로, 커넥션 관리, SQL 실행, ResultSet 처리등의 반복적이고 중복되는 작업들을 대신 처리하고
SQLException → DataAccessException 변환을 담당한다.
Spring Boot는 application.properties 설정을 기반으로
DataSource 및 JDBC 관련 Bean을 자동 구성한다.
spring.datasource.url=jdbc:mysql://localhost:3306/memo
spring.datasource.username=root
spring.datasource.password={비밀번호}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
Gradle 의존성 추가
// MySQL
implementation 'mysql:mysql-connector-java:8.0.28'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
강사님의 비유가 너무 찰떡이라 가져와봤다.
나도 이전에 언어를 배울 때 등 비유를 통해서 외우는 편이었는데, 비유는 확실하게 오해의 소지가 없어야 오개념이 생기질 않아서 요즘은 최대한 모두 이해한 뒤에 비유를 사용하려는 편이 되었다.
나도 이런 정확한 비유를 잘 쓸 수 있으면 좋겠다.
설계 원칙 - 맛있게
디자인 패턴 - 레시피
프레임 워크 - 밀키트
스프링 프레임워크는 DI 디자인패턴으로 IoC를 이루어 낸다.
getter/setter가 있는 단순한 POJO(Plain Old Java Object)
원래는 단순 객체를 의미했지만, 서버에서 관리되는 컴포넌트까지 의미가 확장되었다.
Spring IoC 컨테이너가 생성, 설정, 의존성 주입, 라이프사이클 관리를 하는 객체.
이하 Bean으로 부른다.
Spring 컨테이너가 객체 관리를 하도록 하려면 해당 클래스를 bean으로 등록해줘야 한다.
Spring Boot 앱 실행 시, 메인 클래스에 @SpringBootApplication 애노테이션 설정대로 하위 패키지를 자동으로 스캔하여 Bean으로 등록할 클래스를 탐색한다.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
[참고] @SpringBootApplication는 내부적으로 아래 애노테이션들을 포함한다.
@ComponentScan 지정된 패키지 아래 Bean 후보 스캔
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
}
)
public @interface SpringBootApplication { }
이 탐색에 걸리게? 하려면 @Component, @Controller (RestController), @Service, @Repository 등의 애너테이션을 달아야 Spring이 관리할 대상임을 알고
컨트롤러, 서비스, 레포지토리 등 Bean으로 관리될 클래스들이 자동으로 컨테이너에 등록된다.
해당 필드가 DI 받아야할 대상임을 알려준다.
private 접근제한이 있어도 DI가 된다.
코드 안정성 측면에서 추천되지 않는다.
@Autowired
private AppRepository appRepository;
객체의 불변성을 확보할 수 있기 때문에 일반적으로는 이 방법으로 DI하는 것이 좋다.
클래스 내부에 생성자가 하나뿐이라면 @Autowired 를 생략할 수 있다. (Spring 4.3 버전 이상)
Lombok의 @RequiredArgsConstructor으로 생성된 생성자도 포함한다.
@Autowired
public AppService(AppRepository appRepository) {
this.appRepository = appRepository;
}
@Autowired
public void setAppRepository(AppRepository appRepository) {
this.appRepository = appRepository;
}
필드가 final 일 때엔 생성자만으로 초기화가 가능해서 적용할 수 없다.
@Resource
private AppRepository appRepository;
이름(name) 또는 타입(type) 기준으로 Bean 주입
Spring DI와 거의 동일하게 동작하지만, 표준 자바 기반
@Inject
private AppRepository appRepository;
Spring에서 지원하며, @Autowired와 거의 동일
동일 타입 Bean이 여러 개 있을 때 특정 Bean 선택 주입 가능
@Autowired
@Qualifier("memoRepository")
private MemoRepository memoRepository;
ApplicationContext는 Spring IoC 컨테이너의 핵심 인터페이스 중 하나로, Bean 생성, 의존성 주입, 라이프사이클 관리, 이벤트 처리 등 Bean 관리 기능을 확장한 능동적 관리 객체이다.
BeanFactory를 상속하고 있어서 기본적인 Bean 생성/조회 기능을 포함하며, 그 위에 추가 기능(AOP, 메시지 처리, 환경 설정 등)을 제공한다.
@Component
public class AppService {
private final AppRepository appRepository;
public AppService(ApplicationContext context) {
// 1.'Bean' 이름으로 가져오기
AppRepository appRepository = (AppRepository) context.getBean("appRepository");
// 2.'Bean' 클래스 형식으로 가져오기
// AppRepository appRepository = context.getBean(AppRepository.class);
this.appRepository = appRepository;
}
...
}
강의를 하시는 강사님께서도 Spring의 기능들이 추상적이라 잘 이해가 안 될 것이라며 이야기를 꺼냈는데,
이런 내부적인 동작 원리들을 너무 깊게 파고들어 공부하려하니 당장 어렵게 느껴지는것이고 많은 사람들이 금방 포기하게 된다는 것이라고도 하셨다.
현재 이해가 안되는 부분이 있더라도 전체 흐름을 이해한 후 차근차근 공부를 하면 나중에 이해가 될 거라고 하셨다.
기계적인 학습과 깊은 이해 둘 사이의 밸런스를 잡는것이 나에겐 무척 어렵다.
그래도 계속 공부를, 개발을 이어나가다보면 언젠간 이 밸런스가 잡히는 날이 오겠지 라는 기대를 해본다.