전 회사에서는 스프링과 MyBatis, JPA를 기반으로 작업을 했습니다.
원래는 전부 MyBatis였는데 단순 반복 작업을 줄일 수 있다는 이유로 전부 JPA로 교체하는 작업을 진행했으나,
복잡한 쿼리들은 JPA로 옮기는데 어려움이 있었습니다. QueryDSL을 이용했지만 오히려 더 많은 작업이 필요했고...
결국, 단순한 쿼리들은 JPA를 이용하고 복잡한 쿼리들은 MyBatis로 진행하는 혼종이 되었습니다.
하지만 조사해보니 현재 많은 대기업들은 복잡한 쿼리도 JPA만을 이용해서 충분히 해결하고 있었습니다.
결국 JPA에 대한 이해가 부족해서 그렇다는 결론에 도달했고, 명성이 자자해서 익히 알고있던 모기업 기술이사로 있으신 김영한님의
무료강의를 들어보았습니다. 기초적인 부분을 짚어주는 강의였음에도 불구하고, 부끄럽게도 모르고있던 부분도 있고, 기록으로 남기고 싶었던 부분도 있어서 이렇게 글을 남기게 되었습니다.
강의의 목차는 이렇습니다.
1.프로젝트 환경설정
프로젝트 생성
라이브러리 살펴보기
View 환경설정
빌드하고 실행하기
2.스프링 웹 개발 기초
정적 컨텐츠
MVC와 템플릿 엔진
API
3.회원 관리 예제 - 백엔드 개발
비즈니스 요구사항 정리
회원 도메인과 리포지토리 만들기
회원 리포지토리 테스트 케이스 작성
회원 서비스 개발
회원 서비스 테스트
4.스프링 빈과 의존관계
컴포넌트 스캔과 자동 의존관계 설정
자바 코드로 직접 스프링 빈 등록하기
5.회원 관리 예제 - 웹 MVC 개발
회원 웹 기능 - 홈 화면 추가
회원 웹 기능 - 등록
회원 웹 기능 - 조회
6.스프링 DB 접근 기술
H2 데이터베이스 설치
순수 Jdbc
스프링 통합 테스트 스프링
JdbcTemplate
JPA
스프링 데이터 JPA
7.AOP
AOP가 필요한 상황
AOP 적용
1. 프로젝트 환경설정
우선 스프링 부트 스타터 사이트로 이동해서 스프링 프로젝트를 생성합니다. https://start.spring.io
(프로젝트의 생성은 intellij의 new를 통해서만 했었는데 위처럼 진행하니 신선했습니다. 아직 어떤게 더 좋은지는 모르겠습니다.)
Project: Gradle - Groovy Project Spring Boot: 2.3.x
Language: Java
Packaging: Jar
Java: 11 Project Metadata
groupId: hello
artifactId: hello-spring Dependencies: Spring Web, Thymeleaf
주의! - 스프링 부트 3.0을 선택하게 되면 다음 부분을 꼭 확인해주세요.
1. Java 17 이상을 사용해야 합니다.
2. javax 패키지 이름을 jakarta로 변경해야 합니다. 오라클과 자바 라이센스 문제로 모든 javax 패키지를 jakarta로 변경하기로 했습니다.
3. H2 데이터베이스를 2.1.214 버전 이상 사용해주세요.
스프링 부트 3.0 관련 자세한 내용은 다음 링크를 확인해주세요: https://bit.ly/springboot3
최근 IntelliJ 버전은 Gradle을 통해서 실행 하는 것이 기본 설정입니다. 이렇게 하면 실행속도가 느리기 때문에
다음과 같이 변경하면 자바로 바로 실행해서 실행속도가 더 빨라집니다.
Preferences에서 Gradle을 검색하고,
Build and run using: Gradle IntelliJ IDEA
Run tests using: Gradle IntelliJ IDEA
빌드하고 실행하기
1. ./gradlew build
2. cd build/libs
3. java -jar xxx.jar
4. 실행확인
2. 스프링 웹 개발 기초 - API
@ResponseBody 문자 반환 @Controller
public class HelloController {
@GetMapping("hello-string")
@ResponseBody
public String helloString(@RequestParam("name") String name) {
return "hello " + name;
}
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
}
@ResponseBody 를 사용하면 뷰 리졸버( viewResolver )를 사용하지 않습니다.
대신에 HTTP의 BODY에 문자 내용을 직접 반환합니다.(HTML BODY TAG를 말하는 것이 아님)
@ResponseBody 를 사용하고, 객체를 반환하면 객체가 JSON으로 변환됩니다.
viewResolver 대신에 HttpMessageConverter 가 동작합니다.
기본 문자처리: StringHttpMessageConverter
기본 객체처리: MappingJackson2HttpMessageConverter
byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음
3. 일반적인 웹 애플리케이션 계층 구조
컨트롤러: 웹 MVC의 컨트롤러 역할
서비스: 핵심 비즈니스 로직 구현
리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
이부분이 신기했는데, 아직 데이터 저장소가 선정되지 않은 상황을 가정하고 자바만을 이용해서,
구현체로 가벼운 메모리 기반의 데이터 저장소를 사용했습니다.
회원 리포지토리 메모리 구현체
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
/**
* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
*/
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
public void clearStore() {
store.clear();
}
}
회원 리포지토리 테스트 케이스 작성
강의에서 테스트의 중요성을 강조했습니다. 항상 시간문제로 인해 메소드를 만들면 당장 실행해보고 오류가 없으면 그냥 넘어가곤 했는데,이런 메소드양이 많아지고, 많은 직원들과 공유를 하게된다면 확실하면서 가시성좋은 테스트코드가 있고 없고의 차이는 엄청날것 같습니다.알면서도 잘 안되는 테스트코드의 중요성을 새삼 또한번 느끼게 됩니다. 큰 회사에선 이런 테스트코드에 투자하는 시간이 크다고 합니다. 이제부터라도 테스트코드를 신중하게 짜야겠습니다. 또한 오랜만에 //given //when //then주석을 보니 학원다닐때 생각이 나더군요.
@BeforeEach : 각 테스트 실행 전에 호출됩니다.
@AfterEach : 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있습니다. 이렇게 되면 다음 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있습니다. @AfterEach 를 사용하면 각 테스트가 종료될 때 마다 이 기능을 실행합니다. 여기서는 메모리 DB에 저장된 데이터를 삭제하기위해 사용해줍니다.
테스트는 각각 독립적으로 실행되어야 합니다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아닙니다.
예외를 터뜨려야하는 상황에서 트라이캐치문을 쓰기 애매할때 위와같은 형식으로 적을수있습니다.
람다식을 이용해서 오른쪽것을 실행하면 왼쪽의 클래스가 발생해야된다는 뜻입니다.
단축키 TIP
쉽게 테스트 클래스를 만드는 방법은 테스트를 만들 클래스에서 cmd + shift + T를 해주면,
Create New Test가 뜨고, 들어가면 테스트할 메소드를 @Test메소드로 껍데기를 만들어줍니다.
객체를 복사해서 새로운 객체를 만들경우, 이름이 같아서 오류가 날때 해당 이름에 놓고 shift + f6을 누르면 변경할수있고,
변경하면 밑으로 해당 변수명이 한번에 변경됩니다.
option + cmd + V를 입력하면 값을 객체에 담을수있도록 타입과 변수를 입력할수 있도록 세팅됩니다.
ctrl + R 은 이전에 실행했던 작업을 다시 해주는 단축키입니다.
4. 스프링 빈과 의존관계
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
생성자에 @Autowired 가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어줍니다. 이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI (Dependency Injection), 의존성 주입이라 합니다.
생성자가 1개만 있으면 @Autowired 는 생략할 수 있습니다.
// 오류발생
Consider defining a bean of type 'hello.hellospring.service.MemberService' in
your configuration.
memberService가 스프링 빈으로 등록되어 있지 않아서 발생했습니다.
MemberController는 스프링이 제공하는 컨트롤러여서 스프링 빈으로 자동 등록됩니다. (@Controller 가 있으면 자동 등록됨)
스프링 빈을 등록하는 2가지 방법 : 컴포넌트 스캔, 자바 코드로 직접 스프링 빈 등록하기
이중에 컴포넌트 스캔은 @Component 어노테이션이 있으면 스프링 빈으로 자동 등록됩니다.
@Controller, @Service, @Repository 어노테이션들은 @Component를 포함하기 때문에 스프링 빈으로 자동 등록됩니다.
스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록합니다.(유일하게 하나만 등록해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 인스턴스입니다. 설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용합니다.
자바 코드로 직접 스프링 빈 등록하기
향후 메모리 리포지토리를 다른 리포지토리로 변경할 예정이므로, 컴포넌트 스캔 방식 대신에 자바 코드로 스프링 빈을 설정합니다.
XML로 설정하는 방식도 있지만 최근에는 잘 사용하지 않습니다.
DI에는 필드주입, setter주입, 생성자주입 이렇게 3가지 방법이 있는데 주로 생성자 주입을 권장합니다.
실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리는 컴포넌트스캔을 사용하고, 정형화되지않거나 상황에따라 구현 클래스를 변경해야 하면 설정을 통해 스프링빈으로 등록합니다.
@Autowired를 통한 DI는 스프링이 관리하는 객체에서만 동작합니다. 스프링빈으로 등록하지 않고 개인이 직접 생성한 객체에서는 동작하지 않습니다.
6. 스프링 DB 접근 기술
새롭게 H2를 사용해봤습니다. 오라클이나 MySQL기반만 사용해봤기 때문에 낯설었지만 어렵지 않고 용량도 가벼워서
개발이나 테스트 용도로 저도 자주 사용하려 합니다. https://www.h2database.com/html/download-archive.html
만약 이미 설치하고 실행까지 했다면 다시 설치한 이휴에 ~/test.mv.db파일을 꼭 삭제해주세요.
그렇지 않으면 다음 오류가 발생하면서 접속되지 않습니다.
General error: "The write format 1 is smaller than the supported format 2 https://www.h2database.com
jdbc:h2:~/test (최초 한번) 테이블 생성하기
테이블 관리를 위해 프로젝트 루트에 sql/ddl.sql파일을 생성
~/test.mv.db 파일 생성 확인
이후부터는 jdbc:h2:tct://localhost/~/test 이렇게 접속
다운로드 및 설치
h2 데이터베이스 버전은 스프링 부트 버전에 맞춥니다.
맥사용자는 우선 권한을 줘야 합니다. chmod 755 h2.sh(윈도우 사용자는 x)
./h2.sh로 실행합니다.(윈도우 사용자는 h2.bat)
JPA
build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
스프링 부트에 JPA 설정 추가 resources/application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
스프링부트 2.4부터는 spring.datasource.username=sa 를 꼭 추가해주어야 합니다.
7. AOP: Aspect Oriented Programming
AOP가 필요한 상황
모든 메소드의 호출 시간을 측정하고 싶다면?
공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern)
회원 가입 시간, 회원 조회 시간을 측정하고 싶다면?
회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아닙니다. 시간을 측정하는 로직은 공통 관심 사항입니다.
시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어렵습니다.
시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵습니다.
시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 할까요?
AOP 적용
공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern) 분리
시간 측정 AOP 등록
package hello.hellospring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class TimeTraceAop {
@Around("execution(* hello.hellospring..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("START: " + joinPoint.toString());
try {
return joinPoint.proceed();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("END: " + joinPoint.toString()+ " " + timeMs + "ms";
}
}
}
회원가입, 회원 조회등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리합니다.
강의를 마치며
애초 목적이였던 JPA에 대한 깊은 이해를 할 수 있었던 강의는 아니였습니다.
하지만 꽤 오래 사용하고 있는 스프링의 작동원리에서 으레 짐작으로 그렇게 움직이겠거니 하던 부분을 전문가가 딱 짚어서 설명해주니
속이 시원했고, 부끄럽지만 이름만 들어본 AOP기술의 의미와 간단한 사용예제를 배울수 있었습니다.
이밖에도 H2의 사용이라던지, 알아두면 유용한 단축키 등 앞으로 개발생활에서 뼈가 되고 살이되는 정보들을 많이 알게됐습니다.
그리고 배움의 깊이에 대해서 다시 한번 생각해보게 됐습니다.
여태 배웠던 것들보다 앞으로 알아가야할것이 훨씬 많고, 나아갈 방향 등을 고쳐잡는 계기가 됐습니다.
너무 좋은 강의 잘 들었습니다. 감사합니다.
'개발 > 공부' 카테고리의 다른 글
2023년 SQLD 자격증 합격후기 (0) | 2023.10.06 |
---|---|
2023년 SQLD 자격증 준비 (0) | 2023.07.10 |
2022년 정보처리기사 실기 합격 (0) | 2022.07.18 |
2020년 정보처리기사 일정변경 (0) | 2020.03.20 |