- 수업 시간에 고객의 계좌 정보에 관한 프로그램을 샘플로 만들며 아래의 내용을 배우기로 했다.
- 저작권상 내용을 여기에 담기는 좀 그래서 전반적인 개념에 추가적인 정보를 담아 스스로의 이해를 위해 정리해보려고 한다.
- 강사님의 강의 흐름이 마치 현장 플젝을 해결하는 것 같이 느껴지는데, 이 흐름만 잘 기억해도, 차후 개인 프로젝트를 기획할 때 많은 도움이 될 것 같다.
no
흐름
특징
0
개요
시스템 한 줄 소개 / 활용 기술 요약 / 프로젝트 엔터티 구조 안내 / 주요 제공 기능(API) - 분류
1
프로젝트 생성 및 의존성 추가
spring.io 또는 인텔리J를 통해 스프링 프로젝트 생성, 의존성 추가 * 어떤 기술을 쓸 건지에 따라 의존성은 달라질 수 있다.
2
Lombok
Lombok을 통한 dto 생성
3
HTTP 프로토콜
HTTP 요청에 따라 Client의 요청사항 분석하고 응답하기
4
H2 DB
개발을 할 때 또는 테스트 시에 H2 DB를 통해 개발한다. * RDB와의 소통은 JPA를 사용하도록 한다.
5
트랜잭션
DB의 상태를 변화시키는 작업단위. ACID
6
Embeded Redis
싱글 쓰레드 기반의 일종의 noSql이며 메모리DB를 사용한다. Spinlock으로 동시성을 제어하여 빠르게 휘발적으로 데이터를 사용할 때 Redis를 사용한다.
7
테스트
테스트를 왜 할까? TDD와 단위적 테스트의 차이점은?
1. 의존성 추가하기
- 인텔리j에서 보면, 프로젝트 바로 하위에 "build.gradle"이라는 파일이 있다.
- 앞에서도 잠깐 언급했는데, dependencies 안에 의존성을 추가할 수 있다.
인텔리j에서 플젝 시작할 때 Lombok이나 JDBC 같은 라이브러리를 추가하면 자동으로 의존성이 추가되는데,
별도로 직접 작성해줄 수도 있다.
/* 플러그인의 의존성(library) 관리 */
plugins {
id 'org.springframework.boot' version '2.7.3'
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
id 'java'
}
group = 'com.zerobase'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
/* 각종 의존성들(libraries)을 어떤 원격 저장소에서 받을 것인지 지정 */
repositories {
mavenCentral() // jcenter로 업로드 설정을 간소화할 수 있다.
}
/* 프로젝트 개발에 필요한 의존성들을 선언하는 곳 */
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
2. Lombok
- Lombok은 DTO(=VO) 객체의 Getter/Setter, 생성자를 자동으로 생성해주거나,
특정 클래스를 지정해서 로그를 자동으로 생성해주는 라이브러리이다.
- 다양한 기능
구분
표기
내용
게터/세터 외 다수
@Data
게터/세터/toString/Equals/HashCode 등 한번에 제공
** @Data의 경우, 사용자의 정보가 toString으로 그대로 노출되는 등의 이슈가 발생할 수 있으므로 사용에 주의하는 것이 좋다.
- 한 컴퓨터에서 다른 컴퓨터로 데이터를 요청할 때에는 데이터를 패킷이라는 단위로 묶어 주는 과정을 거친다. * OSI 7 Layer : Application -> Presentation -> Session -> Transport -> Network -> Data Link -> Physical * TCI/IP : Application -> Transport -> Internet -> Network Interface WWW는 TCP/IP방식을 따르므로, 그를 기준으로 보면 HTTP는 Application 단계(최상위 단계)에 해당된다. - Application단계에서 Client는 URI로 요청사항을 날리고, 그 요청사항은 HTTP의 헤더와 바디에 담겨진 뒤, 나머지 하위 과정에서 포장된다. 포장된 데이터는 여러 네트워크망을 거쳐 Server로 넘어가게 된다.
- HTTP는 POST/GET/PUT(PATCH)/DELETE 라는 요청방식을 가진다.
- HTTP는 본래 Connectless, 한 번 연결하면 끝나는 일회성 연결 방식을 가졌으나, HTTP 1.1로 버전이 올라가면서 keep-alive를 통해 일정 기간 동안 연결 상태를 유지하게 되었다. * TCP/IP 방식에 따르면, 매번 연결할 때마다 3-way-handshake가 필요하다. 1.1에서는 일정 기간 이걸 하지 않는다. 요즘에는 또 HTTP 2.0 버전이 나왔는데, 가장 큰 특징은 1.1은 한 번에 하나의 파일만 전송 가능했다면, 2.0은 이제 여러개 파일을 병렬처리로 전송하는 Multiplexing이 된다는 것. (그러나 헤더와 바디의 큰 구조는 위 그림처럼 비슷하다)
- 추가적으로, HTTP는 본래 Stateless,통신이 끝나면 상태를 유지하지 않았지만 쿠키와 세션으로 상태를 유지할 수 있다. 특정 정보(ex.로그인 정보)를 쿠키는 브라우저에, 세션은 서버의 id를 이용해 암호화해 서버에 저장 후 브라우저에도 저장한다. * 단, 연결 상태 유지 기간을 설정하면, 데이터는 브라우저가 아닌 로컬에 저장된다.
- 정리하면, 오늘날의 HTTP는 URI를 통해 요청을 받는 것은 동일하되, 일정 기간 동안 연결을 유지할 수 있고, 데이터 송수신 시 병렬 처리가 가능해졌으며, 쿠키와 세션을 통해 특정 상태(ex. 로그인 상태)를 브라우저(또는 로컬)에 저장하여 유지시킬 수 있다.
(1) HTTP Request
POST /path HTTP/1.1 // Header 첫줄 : Method 경로 HTTP버전
Content-Type: application/json // 받은 콘텐츠 타입
Accept: application/json // 보낼 콘텐츠 타입 (응답)
UserInfo: {"userId","test1234"} // 인증키나 회원명 등을 넣는다
{ // Body
"phone" : "010-1234-5678"
}
(2) HTTP Response
HTTP/1.1 200 OK // Header 첫줄 : HTTP버전 상태정보 메세지
Content-type: application/json // content-type부터 연결(keep-alive)유지 기간 등
Transfer-Encoding: chuncked
Date: Mon, 21 Jun 2022 12:11:23 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{ // Body : 응답 받는 Clinet에게 전달할 데이터
"message" : "등록 성공"
}
4. H2 DB를 사용하기
(1) H2 DB의 특징
: 메모리/파일 관계형 DB로, 가볍고 시작할 때마다 자동 삭제-생성 가능하며, 대다수의 DB sql명령어와 호환되어,
: 스프링 플젝을 처음 생성하면, [src] - [java] - [main] - [resources] 안에 "application.properties" 파일이 있다.
: 애플리케이션에 들어가는 설정들을 정리해서 넣는 파일인데, 이 파일의 확장자를 .properties에서 .yml로 바꾼다.
rf. .yml파일은 야믈이라고도 하는 yaml 데이터 타입을 말한다. (+ 자세한 내용은 하단의 더보기에)
: 아래의 내용을 보면, 스프링 설정을 야믈 데이터 타입으로 안내하는 것인데 크게 보면,
[ DB에 대한 설정(위치와 JDBC정보) + H2 콘솔 사용 여부 + JPA 관련 설정(INSERT시작 지점, DB정보, 쿼리 관련 설정) ]
이렇게 세가지를 아래에서 하고 있는 걸 볼 수 있다.
spring: // spring 플젝의 설정할거다!
datasource: // (1) DB의 소스 :
url: jdbc:h2:mem:test // 5가지 정보(url-ip와port와instance),계정,비번)
username: // -- ip와 port대신 mem (memory를 의미)
password:
driverClassName: org.h2.Driver // JDBC 드라이버 정보
h2: // (2) h2 console 사용 여부
console:
enabled: true
jpa: // (3) jpa 관련 설정
defer-datasource-initialization: true // table 생성 후로 resource의 data.sql 입력 미룬다(defer)
database-platform: H2 // db vendor
hibernate: // hibernate (jpa 인터페이스 구현체)
ddl-auto: create-drop // ddl-auto : 자동으로 실행시 drop, create
open-in-view: false // open-view (뷰에서 볼 것인지)
properties: // properties (설정)
hibernate: // hibernate - sql 형식으로 쓰고 볼지
format_sql: true
show_sql: true
[ JPA란 ? ]
- 출처 : https://dbjh.tistory.com/77?category=853400 - JPA란, Java Persistence API의 약자로, 말 그대로 RDB와 소통할 때도 자바를 지속적으로 쓰게 해주는 API이다. - 조금 어려운 말로는, 자바에서 ORM(Object-relational Mapping) 기술 표준으로 사용되는 인터페이스 모음이라고 하는데, - 결국 ORM도 객체와 RDB를 연결(매핑)해주는 걸 의미한다. - JPA는 JPA라는 인터페이스가 있고, 그를 hibernate, OpenJPA등이 구현한다.
- 장점 : - SQL문이 아닌 자바언어로 DB를 조작하기 때문에, 비즈니스 로직에 맞춰 개발하기 편리하며 가독성이 높다. - 객체 지향적인 코딩이 가능해 진다. (상속 또한 RDB로 자동 표현할 수 있게 했다.) - DB vendor를 변경해도 코드를 수정하지 않아도 되어 vendor에 대한 의존도를 낮추고 리팩토링에 유리하다.
- 단점 : - 프로젝트 규모가 크고 복잡한 상태일 때, 설계가 잘못 될 경우, 속도 저하 및 일관성이 떨어질 수 있다. - 복잡하고 무거운 쿼리문의 경우 속도를 위해 별도로 SQL문을 써야할 수 있다. - JPA를 배우는 비용이 비싸다.
- 설정을 다 하고, 어플리케이션을 이클립스나 인텔리j에서 실행하면, 에디터 콘솔에 Tomcat을 통해 알아서 잘 생성됐다는 메세지가 뜬다.
- 이제, 브라우저에서 아래의 정보를 입력하면 H2 DB 콘솔을 볼 수 있다.
http://localhost:8080/h2-console
- 설정 야믈에 써놓은 URL과 계정명, password를 잘 써주고 연결하면 된다. (JDBC URL이, url이다)
* 처음에 JDBC URL은 jdbc:h2:~/test;로 되어 있는데, 이 부분을 아까 야믈 설정파일에서 설정해두지 않았다면,
매번 실행할 때마다 주소가 자동 할당되어 에디터 콘솔에 뜨고, 그걸 복사해서 위에 붙여주어야만 한다.
: 레이어 패턴에서는 dto를 domain안에 만들었는데, @Entity를 통해 이 클래스가 테이블임을 표현한다.
: 이렇게 생성한 후에 다시 애플리케이션을 실행하면, H2 console 부분에 테이블이 생성된 게 보인다.
// import 생략
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder // 여기까지 Lombok을 사용해 builder, 게터/세터를 만들게 한다.
@Entity // Entity를 쓰면 이 클래스를 Entity로 설정하겠다는 의미이다.
public class TableName{
@Id // pk인덱스 생성
@GeneratedValue // pk가 Sequence인지, Table인지 등과 세부 제한 설정
private Long id;
private String certainMember1;
private String certainMember2;
}
(3-2) 리포지토리 만들기
- JPA를 통해 DB와 직접적인 소통을 하는 것은 repository interface이다.
- 중요한 건, 이것은 인터페이스이며 클래스로 만들면 안 된다는 것이다.
* 왜 인터페이스로 되어 있을까? 그건 DIP 법칙에 따라 JPA에 너무 의존하지 않게 하려는 것 아닐까? (좀더 생각해보자..)
- JpaRepository<테이블객체명,pk타입>를 구현한 인터페이스를 만든다.
@Repository
public interface CertianRepository extends JpaRepository<TableName, Long> {
}
- JpaRepository의 메소드들은 굉장히 많았는데 그 중에 기본적인 메소드들은 아래와 같았다.
save(S entity) : S saveAll(Iterable<S> entities) : Iterable saveAll
- 바로 위의 표의 메소드들 (save, findById, delete, count 등)을 호출하는 클래스를 만든다.
@Service
@RequiredArgsConstructor
public class CertainService {
private final CertainRepository certainRepository;
@Transactional
public void createOneRow() {
TableName tableName = TableName.builder()
.memberfield("hahaha")
.build();
certainRepository.save(tableName);
}
@Transactional
public TableName getOneRow(Long id) {
return certainRepository.findById(id).get();
}
}
(3-4) 컨트롤러로 URI에 대한 HTTP 처리하기
- 앞서서 Client는 브라우저의 URI를 통해 요청을 보낸다고 했다.
- 이제 어느 경로에서 어떻게 URI를 보내면, 특정 서비스의 메소드를 호출할지를 여기서 정해주면 된다.
@RestController
@RequiredArgsConstructor
public class CertainController {
private final CertainService certainService;
@GetMapping("/create-certain")
public String createCertain() {
certainService.createOneRow();
return "success";
}
@GetMapping("/certain/{id}")
public Account getAccount(@PathVariable Long id) {
return certainService.getOneRow(id);
}
}
5. 트랜잭션
트랜잭션 : 데이터베이스의 상태를 변경시키기 위해 수행하는 작업단위
[출처] 코딩팩토리, https://wonit.tistory.com/462
- 트랜잭션은 DB의 상태를 변경시키는 것으로, CRUD(INSERT, SELECT, UPDATE, DELETE) 행위를 말한다.
(1) Commit과 Rollback
- 트랜잭션은 순차적으로Commit -저장-을 하며 실패해도 로그를 남겨, 앞의 트랜잭션이 다 끝나야 실제로 반영한다.
- 만약에, 트랜잭션이 비정상적으로 종료한다면 Rollback -철회-을 통해, 트랜잭션 전체 또는 부분적으로 결과를 취소한다.
(2) 트랜잭션의 상태
- 트랜잭션은 실행중(Active)이거나
- 커밋 일부 완료(Partially Commited), 커밋 모두 완료(Commited)
- Redis 또한 in-memory 방식의 noSql이기 때문에 다른 DB들과 마찬가지로 host와 port가 필요하다.
- 앞에서 만들어주었던 application. 야믈파일에서 아래와 같이 호스트와 계정을 넣어준다.
* 보통 Redis는 6379 포트를 쓴다고 한다.
spring:
redis:
host: 127.0.0.1
port: 6379
(2) LocalRedis 실행 설정하기
- Config 클래스를 하나 만들어, 포트정보를 통해 RedisServer를 만들어준다.
- RedisServer는 @PostConstruct와 @PreDestroy를 할 수 있는 메소드를 생성해주는데,
Bean의 생성주기에서 빈 생성 및 의존관계 주입 후 @PostConstruct가 이루어지고, 종료 전에 @PreDestroy가 이루어진다.
[ Bean의 생성주기 ]
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백(EVENT) → 앱 본연의 동작 수행 → 소멸전 콜백(EVENT) → 스프링 종료
@Configuration
public class LocalRedisConfig {
@Value("${spring.redis.port}")
private int redisPort;
private RedisServer redisServer;
@PostConstruct
public void startRedis() {
redisServer = new RedisServer(redisPort);
redisServer.start();
}
@PreDestroy
public void stopRedis() {
if (redisServer != null) {
redisServer.stop();
}
}
}
(3) LocalRepository 등록
- Config 클래스를 하나 만든 후에, 호스트와 포트 맴버 필드를 만들고, RedisClient를 호출하는 메소드를 만든다.
- 이 설정 자체도 너무 어려웠다.. 암기가 필요한 순간.
@Configuration
public class RedisRepositoryConfig {
@Value("${spring.redis.host}") // EL같다. application의 데이터를 가져온다.
private String redisHost;
@Value("${spring.redis.port}") // EL같다. application의 데이터를 가져온다.
public int redisPort;
@Bean
public RedissonClient redissonClient() { // RedissonClient을 반환해주어야한다.
Config config = new Config(); // Config 객체 생성
config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
return Redisson.create(config);
}
}
7. 테스트
- 과거의 경우 어떤 서비스가 만들어지면 그것을 하나하나 검수하는 과정을 거쳤었다.
- 현재에는 자동화된 테스트 코드를 통해 빠르게 테스트를 하고 있다.
- 다양한 테스트 커버리지와 테스트 방법론이 존재한다.
과거의 테스트
현재의 테스트
방법
기능적으로 하나하나 일일히 검수
자동화된 테스트 코드
특징
다수의 인력, 시간이 너무 많이 든다
빠른 속도로 테스트 가능, 코드 수정 후 재검수 용이
[ 테스트에 관한 개념 ]
(1) 테스트 커버리지 : 단위 테스트 / 전 구간 테스트 / 통합 테스트
단위 테스트(Unit Test)
전 구간 테스트(End-to-End Test)
통합 테스트(Integration Test)
크기
클래스 또는 메소드
현재부터 ~ 배포까지
연관된 모든 기능 전반(외부lib까지)
장점
- 개발자 관점의 빠른 테스트 - TDD와 함께 할 때 강력하다.
- 내부 기능까지 테스트하지 않는다 - 사용자 관점에서 E2E(End to End) 형식을 이용해 확인
- 개발자가 변경불가한 부분 확인 - 단위 테스트에서 발견이 어려운 버그까지 커버
단점
- 단위를 벗어난 영역의 버그를 확인하기 어려움
- 테스트를 만들기가 힘들다 - 많은 코드를 테스트해 신뢰성 ↓
- 많은 코드를 테스트해 신뢰성 ↓ - 에러 발생 지점을 찾기 어렵다 - 유지보수가 힘들다
(2) TDD란 뭘까? Test-Driven-Development의 약자로, "테스트 주도 개발"이라는 테스트 방법론을 의미한다.
: 간단히 요약하자면, 개발 전에 테스트 코드부터 작성하고 개발 후 리팩토링(수정)하는 방법이다.
(3) 테스트는 왜 해야할까? 비즈니스 로직에 맞고, 빠르고 안정적인 개발을 하기 위함
1) 테스트를 하면서 스스로 자신의 코드를 리뷰할 수 있다.
2) 테스트가 잘 되어있으면 리팩토링 하기가 수월하다. - 개발을 하다보면, 기존에 만들어진 코드가 정책에 맞춘 코드인지, 아니면 임의로 만들어진 코드인지 구분을 해야한다. - 테스트 코드를 보면 기획에서 다 알지 못하는 영역까지도 코드 내역을 통해 커버할 수 있다.
(4) 테스트를 잘 하기 위해서는 뭘 해야할까?
1) 클래스나 메서드가 SRP를 지키며 너무 크지 X
2) 단위 테스트 시, 적절한 Mocking으로 격리성 확보 -- Mocking이란, 카피를 의미
3) 테스트 커버리지를 높혀, 놓치는 구간이 없도록 한다.
4) 테스트 코드 또한 속도 및 방법 등의 면에서 지속적으로 개선해준다.
[ 테스트를 위한 라이브러리 - JUnit, Mockito ]
JUnit5
- xUnit이라는 단위 테스트(Unit Test) 프레임워크
- 단위 테스트를 실행 후 전체 결과를 리포트
- 스프링 2.4버전대부터 spring-boot-starter-test에 JUnit5가 포함되어 있다. * 최근 버전에서 JUnit4를 선택하면 테스트 동작안한다. (잦은 테스트 에러)
Mockito
- Mock(가짜)을 만들어 주는 라이브러리
- 왜 필요한가? : 특정 controller나 service에 연관된 클래스들은 main에 작성되어 있다. : 만약 test를 위해 main과 test를 둘 다 돌리면 port를 두 개의 프로그램이 사용하는 것으로 에러가 난다. : 그렇기에 main에 있는 의존하는 클래스를 test 파일 안에 일일히 생성해주어야 하는데 : 이러한 방식은 품이 너무 많이 든다. --> Mock을 만들어, 의존 클래스를 inject 한다.
[ JUnit5와 Mockito를 통한 단위 테스트 실습 ]
(1) JUnit5를 사용해보기
(1-1) build.gradle의 dependencies에 "spring-boot-starter-test"가 있는지 확인 (디폴트로 되어있다)
- 위에 보면 tasks.named('test')가 있는데, 'test'로 된 디렉토리의 테스트들을 JUnit으로 돌리겠다는 의미이다.
(1-2) 테스트하고자 하는 클래스에서 Ctrl+shirt+T로 테스트를 생성한다
- 보면 아래처럼 JUnit5로 라이브러리가 설정되어 있는 걸 볼 수 있다. 이걸 4로 바꾸면 당연히 테스트는 안 된다.
(1-3) @SpringBootTest와 @Autowired를 통해 의존을 주입한다.
// import 생략
@SpringBootTest // 실제 환경과 동일하게 모든 Bean들을 등록
class CertainTest{
@AutoWired // 주입
CertainService certainService;
@BeforeEach // 아래의 두 메소드 각각 시작 전에 행 하나 생성
void init() { certainService.createOneRow(); }
@Test
@DisplayName("Test Name blah blah")
void testGetOneRow(){
TableName tableName = certainService.getOneRow(1L);
assertEquals("010-1234-5678", tableName.getPhone());
assertEqauls(TableName.CertainType, tableName.getType());
}
@Test
@DisplayName("Test Name blah blah2")
void testGetOneRow2(){
TableName tableName = certainService.getOneRow(2L);
assertEquals("010-1234-5678", tableName.getPhone());
assertEqauls(TableName.CertainType, tableName.getType());
}
}
- 그런데 위와 같이 하면 @SpringBootTest로 안 쓰는 클래스까지 가져오면 원하던 테스트에서 방향이 달라질 수 있다.
- 더불어 매번 @AutoWired로 주입을 하게되면 너무 품이 많이 든다.
- 이를 해소하고자 Mockito의 Mock을 사용한다.
(2) Mockito의 Mock을 사용해서 service에서 필요한 클래스만 가짜로 주입하기
@ExtendWith(MockitoExtension.class) // MockitoExtension이라는 클래스를 쓴다.
class CertainServiceTest {
@Mock // 가짜 생성
private CertainRepository certainRepository;
@InjectMocks // 위에서 만든 Mock을 주입
private CertainService certainService;
@Test
@DisplayName("성공")
void testXXX() {
// given
given(certainRepository.findById(anyLong()))
.willReturn(Optional.of(TableName.builder()
.type(TableNameType.TYPE)
.phone("010-1234-5678").build()));
// when
TableName tableName = certainService.getOneRow(1234L);
// then
assertEquals("010-1234-5678", certainService.getPhone());
assertEquals(TableNameType.TYPE, certainService.TableNameType());
}
}
[ Controller 테스트 하기 ]
방법1
방법2
@SpringBootTest + @AutoConfigMockMvc
: 전체 Bean 생성 후
: mockMvc로 HTTP요청 및 검증
@WebMvcTest
: 필요로하는 MVC관련 Bean만 생성 - Controller, ControllerAdvice, Converter, Filter 등
: 하위 레이어 기능*의 경우, @MockBean으로 mocking - *Controller에 의존하는 Service 및 기타
: 마찬가지로 mockMVC로 HTTP 요청 및 검증
@WebMvcTest(AccountController.class)
class AccountControllerTest {
@MockBean
private AccountService accountService;
@MockBean
private RedisTestService redisTestService;
@Autowired
private MockMvc mockMvc;
@Test
void successGetAccount() throws Exception{
// given
given(accountService.getAccount(anyLong()))
.willReturn(Account.builder()
.accountNumber("3456")
.accountStatus(AccountStatus.IN_USE)
.build());
// when
// then
mockMvc.perform(get("/account/876"))
.andDo(print())
.andExpect(jsonPath("$.accountNumber").value("3456"))
.andExpect(jsonPath("$.accountStatus").value("IN_USE"))
.andExpect(status().isOk());
}
}
package com.example.websample.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class ErrorResponse {
private String errorCode;
private String message;
}
- 위에서 작성했던 예외처리 코드를 변경하여 객체로 묶는다.
@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(IllegalAccessException.class)
public ErrorResponse handleIllegalAccessException(
IllegalAccessException e){
log.error("IllegalAccessException is occured.", e);
return new ErrorResponse("INVALID_ACCESS",
"IllegalAccessException is occured.");
}
- 내부적으로 아래와 같은 메소드들이 있는데 이들 모두 InterceptorResgistration을 반환하므로 메소드 체이닝을 한다.
- 메소드들
addInterceptor()
인터셉터 추가
order()
순번
addPathPatterns()
어떤 url패턴(경로)에서 실행할 건지
excludePathPatterns()
일반적으로 css나 images와 같이 정적 페이지들을 대상으로 제외처리
package com.example.websample.config;
// import 생략
@Configuration
public class WebConfig implements WebMvcConfigurer{
// Filter 내용 생략
// Interceptor 내용
@Override
public void addInterceptors(InterceptorRegistry registry){
// registry : 등록 내용 관리부
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/*", "/images/*");
}
}
>> 기존의 컨트롤러에 예외 사항을 임의로 추가
@GetMapping("/order/{orderId}")
public String getOrder(
@PathVariable("orderId") String orderId,
@RequestParam("orderAmount") Integer orderAmount) throws IllegalAccessException {
log.info("Get some order");
if ("500".equals(orderId)) {
throw new IllegalAccessException("500 is not valid orderId");
}
return "OrderId : " + orderId + ", orderAmount : " + orderAmount;
}
>> 요청
http://localhost:8080/order/500?orderAmount=1000
>> 로그
// 필터 진입
Hello LogFilter : Thread[http-nio-8080-exec-1,5,main]
// preInterceptor
preHandle LogInterceptor: Thread[http-nio-8080-exec-1,5,main]
preHandle handler: com.example.websample.controller.SampleController#getOrder(String, Integer)
// handler
Get some order
// ! postInterceptor로 가지 않았다.
// afterInterceptor
afterHandle LogInterceptor: Thread[http-nio-8080-exec-1,5,main]
afterCompletion exception : 500 is not valid orderId
// exception 메세지 - 오류 코드 나머지는 생략
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalAccessException: 500 is not valid orderId] with root cause
java.lang.IllegalAccessException: 500 is not valid orderId
// 브라우저에 페이지 로딩하며 다시 실행
preHandle LogInterceptor: Thread[http-nio-8080-exec-1,5,main]
preHandle handler: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
postHandle LogInterceptor: Thread[http-nio-8080-exec-1,5,main]
afterHandle LogInterceptor: Thread[http-nio-8080-exec-1,5,main]
| 정리
- 필터, 인터셉터, AOP는 모두 공통 기능을 처리하기 위해 사용된다.
- 주로 필터, 인터셉터는 웹에 관한 처리(ex.URL주소 및 프로토콜)를 하며, AOP는 자바 코드에 관해 처리한다.
- 필터는 Filter 인터페이스를 구현하고, doFilter()안에서 chain.doFilter(request, response)를 작성하여 만들 수 있다.
- 인터셉터는 HandlerInterceptor 인터페이스를 구현하고, preHandle()/postHandle()/afterCompletion()을 작성하여 만들 수 있다.
- 필터와 인터셉터 모두 @Configuration 클래스에서 @Bean으로 등록해주는 것이 필요한데,
- 필터의 경우, FilterRegistrationBean 클래스를 만들고 setFilter()/setOrder()/addUrlPatterns()를 설정한다.
- 인터셉터의 경우, WebMvcConfigurer를 구현한 후, addInterceptor()를 통해 이미 등록된 registry의 메소드들을 사용한다.
[1] Dispatcher Servlet이 요청 URL을 받아 HandlerMapping에 전달 * Dispatch : 파견 보내다. rf. 도서관의 사서(dispatcher)는 도서를 요청(request)받았고, 도서 코드 목록(HalderMapping)서 도서를 어떻게 찾을지 본다.
[2] HandlerMapping는 요청 URL에 맞는 Controller와 Method 정보를 반환 rf. 도서의 코드 목록에 따르면 해당 도서는 '역사' 카테고리 구역에 있단다.
[3] Dispatcher Servlet이 Handler Adapter에게 요청 처리를 위임 rf. 사서는 인턴에게 '역사' 카테고리 책장으로 이동해서, 가나다순으로 진열된 책들을 보면 된다고 알려준다.
[4] HandlerAdapter가 Controller와 Method를 실행 rf. 인턴이 찾으려는 도서의 코드와, 사서의 안내를 가지고 '역사' 구역에 가서 도서를 찾는다.
[5] Cotroller는 비즈니스 로직을 처리하고, 그 결과를 바탕으로 뷰(ex.JSP)에 전달할 객체를 Model 객체에 저장 rf. 인턴은 찾은 도서를 가져와 책수레에 담는다.
[7] Dispatcher Servlet은 View 객체에 화면 표시를 의뢰 rf. 사서는 책을 요청한 사람에게 책을 전달하기 위해 "00책 요청하신분~?"하고 물어본다.
[8] View 객체는 해당하는 뷰(ex.JSP.Thymeleaf)를 호출하며, 뷰는 Model 객체에서 화면 표시에 필요한 객체를 가져와 화면 표시를 처리 rf. 사서는 책을 요청한 사람에게 책을 대여해주고, 반납일에 대해 안내한다.
| HTTP 요청 - RestController
- HTTP 요청 및 응답은 Rest 방식에 따른다.
구분
의미
SQL
REST API
CREATE
삽입
INSERT
PUT/POST
READ
조회
SELECT
GET
UPDATE
갱신
UPDATE
PUT/PATCH
DELETE
삭제
DELETE
DELETE
- 데이터 타입은 일반적으로 JSON 타입을 많이 사용한다. (XML보다 경량)
- Controller와 RestController의 차이
Controller
기본적으로 HTML으로 응답값 전달
RestController
Rest API 요청에 따른 응답값(ex.JSON) 전달
- 웹을 통해서 받는 URI의 경우 아래와 같이 [URL + 전달하려는 데이터]로 구성되어 있다.
URI 예시
https://why-dev.tistory.com/252?category=964854
- RestController를 만들어주기 위해서는 아래와 같이@RestController를 작성해주어야 한다.
@RestController
public class SampleController{
}
+ 여기에 만약에 롬복을 추가할 경우,
@Slf4j
@RestController
public class SampleController{
}
| HTTP요청 매핑
[1] Mapping Annotation
- 요청방식을 직접 지정 (GET,POST...)
// GET, POST 등의 요청방식을 직접 지정
@RequestMapping(value = "/page/252", method = RequestMethod.GET)
public String getPage(){
log.info("Get some page information");
return "page:252"; // JSON 방식
}
- HTTP Mapping을 할 때에는 @GetMapping, @PostMapping, @DeleteMapping.. 등의 축약형 매핑 Annotation을 쓴다. * 이때, Put은 전체를 수정할 때, Patch는 일부를 수정할 때에 사용한다.
- HTTP 요청 파라미터를 전송할 때, (1) Get과 Delete의 경우 - @PathVariable로 고유한 id를 받고, @RequestParam으로 parameter를 받는다. (2) Post, Put, Patch의 경우 - @RequestHeader와 @RequestBody를 통해 parameter를 받을 수 있다. * RequestBody를 쓸 경우, 큰 데이터를 쉽게 받을 수 있다.
프레임워크에서는IoC(Inversion of Control)곧,제어의 역전이라 하여 프레임워크 자체가 흐름을 통제한다.
반면, 라이브러리의 경우, 흐름을 작성하는 사용자(개발자)에 따라 그 방향성이 달라질 수 있다.
[ SOLID 원칙 ]
- SRP : 단일 책임 원칙
- OCP : 개방 폐쇄 원칙 (인터페이스를 통한 사용 기능의 교체 가능성)
- LSP : 리스코프 치환 원칙 (상위 -> 하위 상속된 메소드는 동일 기능이여야한다.)
- ISP : 인터페이스 분리 원칙 (인터페이스도 단일 책임을 져야한다)
- DIP : 의존성 역전 원칙 (하위의 변경이 상위의 변경까지 요구하지 않도록 인터페이스를 통해 의존성을 끊어버린다)
| 스프링의 코어(핵심)
1. DI, IoC, 컨테이너
- 스프링에서는 컨테이너라는 공간 안에 클래스들을 규격화된 Bean형태로 저장한다.
* 수업에서는 컨테이너를 레고판으로, Bean을 레고에 비유했다.
- 이렇게 규격화된 방식을 사용하게 되면 프로젝트의 개발자가 바뀌어도 작업이 원활하게 이루어질 수 있다.
(1) DI(Dependency Injection) - 의존성 주입
✅ DI 곧, 의존성 주입은 A와 B의 의존 관계를 외부에서 주입해주는 걸 말한다.
(2) IoC(Inversion Of Control) - 제어의 역전
✅ IoC 곧, 제어의 역전이란, 사용자가 클래스를 생성(제어)하지 않고 스프링 프레임워크가 제어하게 하는 것이다.
(3) DI 설정 방법의 역사
✅ DI 설정 방법의 역사 1️⃣ XML을 통한 Bean 등록 2️⃣ XML ComponentScan을 통한 Bean 등록 3️⃣ JavaConfig Class를 통한 Bean 등록 4️⃣ JavaConfig Class의 ComponentScan를 통한 Bean 등록
- DI 설정방법은 XML을 통할 수도 있고, JavaConfig 클래스를 통할 수도 있는데 후자를 많이 사용한다.
- JavaConfig를 사용하는 방법만 보면, 아래와 같이 @Configuration을 통해 이 클래스가 Di config임을 알려주고,
내부에 사용하려는 각 클래스의 구현체를 @Bean을 통해 전달하는 코드를 담는다.
@Configuration
public class ApplicationConfig{
@Bean
public MyService myService(){
return new MyService();
}
}
- 클래스를 Bean으로 등록할 때에는 일일히 @Bean을 치지 않고각 클래스에 @component를 하면 Bean으로 등록된다.
- 거기에 플러스로 config 파일에 @ComponentScan(//생략)을 추가하면 스프링이 알아서 의존 관계를 찾아준다.
@Configuration
@ComponentScan(basePackages = "com.sample.myApplication") // 패키지 기준으로 탐색
public class ApplicationConfig{
}
// 또는
@Configuration
@ComponentScan(basePackageClasses = MyApplication.class) // 메인 클래스 기준으로 탐색
public class ApplicationConfig{
}
- main함수에서 설정을 호출할때
// DI config 호출
// 1. xml을 사용할 경우
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("spring-config.xml");
// 2. config클래스를 사용할 경우
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(ApplicationConfig.class);
[ Bean 설정 방법 - 더보기 ]
구분
설정 방법
설명
구현체 지정
@Primary
구현체에 직접 지정
@Qualifier("beanName")
사용자인 클래스의 생성자 매개변수에 구현체 지정
Set이나 List로 모두 받기
사용자인 클래스의 생성자 매개변수에 Set, List로 지정
프로퍼티 이름을 Bean과 동일하게
ex. PayByCard --> payByCard
Scope
싱글톤
default, 한 번 만들어 계속 사용
Prototype
Request, Session, WebSocket
Profile
@Profile("!production")
여러 환경(test, stage, open)에서만 동작하는 Bean을 만들 때 클래스 단위나 메소드 단위에 사용 가능 - Dspring.profiles.active = sandbox, beta, production 대체로 !, &, | 와 함께 사용
@Component
public class ConveniencePayService { // 편결이
private final Map<PayMethodType, PaymentInterface> paymentInterfaceMap
= new HashMap<>();
private final DiscountInterface discountInterface;
// 생성자를 통해 결제수단의 종류를 Map으로 저장
public ConveniencePayService(Set<PaymentInterface> paymentInterfaceSet,
@Qualifier("paymentByMethod")
DiscountInterface discountInterface) {
paymentInterfaceSet.forEach(
paymentInterface -> paymentInterfaceMap.put(
paymentInterface.getPayMethodType(),
paymentInterface
)
);
this.discountInterface = discountInterface;
}
}
(3) Set 또는 List로 모두 받는다.
(4) 프로퍼티 이름을 Bean과 동일하게 한다.
@Component
public class ConveniencePayService { // 편결이
private final Map<PayMethodType, PaymentInterface> paymentInterfaceMap
= new HashMap<>();
private final DiscountInterface discountInterface;
// 생성자를 통해 결제수단의 종류를 Map으로 저장
public ConveniencePayService(Set<PaymentInterface> paymentInterfaceSet,
DiscountInterface discountByPayMethod) {
paymentInterfaceSet.forEach(
paymentInterface -> paymentInterfaceMap.put(
paymentInterface.getPayMethodType(),
paymentInterface
)
);
this.discountInterface = discountByPayMethod;
}
}
[2] Scope설정하기
- 싱글톤 : default설정(아무것도 설정X시)
- Prototype : 데이터를 클렌징할 때 사용하며, 매번 새로 Bean을 생성한다.
ㄴ Request/Session/Web Socket
[3] Profile
- 인텔리j의 경우, 실행버튼 좌측에 셀렉박스 선택 - [Edit Configuration...] 에서 개발 환경변수를 입력할 수 있고
- 쉘에 직접 칠 경우, - Dspring.profiles.active = sandbox, beta, production 로 개발 환경변수를 입력할 수 있다.
- 특정 환경에서만 사용할 클래스에 @Profile("test")와 같이 작성하면 된다.
@Component
@Profile("test")
public class DiscountByConvenience implements DiscountInterface{
}
✅ 외부자원은 외부 sftp, http, 파일 등에서 얻는다. (pf. sftp는 ssh의 ftp 버전) ✅ Resource는 인터페이스며 다양한 구현체를 갖고 있다. ✅ ApplicationContext는 ResourceLoader를 상속하며 프로그램이 시작할 때 Resource는 자동 로딩된다. ✅ 단, 추가적으로 파일을 로딩해야하는 경우 @Service와 @Autowired를 통해 직접 로딩할 수 있다. ✅ ApplicationContext는 ResourcePatternResolver도 상속하는데, 위치 지정자 패턴(classPath, file, http)에 따라 Resource 구현체가 자동 선택된다.
@Service
public calss ResourceService{
@AutoWired
ApplicationContext ctx;
public void setResource(){
Resource myTemplate = ctx.getResource("classPath:some/resource/path/myTemplate.txt");
}
}
- ApplicationContext(스프링의 핵심설정)을 이루는 설정값을 가져오기
// 스프링의 핵심설정을 이루는 설정값을 가져오기
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml");
ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
Bear bear = (Bear) ctx.getBean("bear");
(2) AOP(Aspect Orient Programing, 관점지향 프로그래밍)
✅ 단순 자바 OOP로는 반복적인 메소드를 관리하기 어려워, AOP를 사용하게 되었다. ✅ AOP로 공통 관심사(로깅, 트랜잭션, 인증)를 여러 메소드 Before/After/Around에 원할 때마다 쉽게 추가할 수 있다. ✅ AOP의 기본 개념 - [ Aspect(관심사) : Pointcut(포인트 선택 방법 - 조건식) + Advice(조언) ] - Join point(연결 포인트) : 프로세스 내의 삽입 지점 - AOP proxy : target object에 advice를 덧붙이기 위해 하는 작업 - Weaving : advice를 비즈니스 로직 코드에 삽입하는 것 ✅ AOP는 AspectJ 라이브러리를 사용해야 제대로 쓸 수 있다.
* 로깅/트랜잭션이란?
- 간단히 말해 로깅은 로그(log) 곧, 기록을 남기는 것을 말하고,
- 트랜잭션이란 DB관련 작업 단위를 말하는 것으로 DB와의 작업을 한다는 의미이다.
OOP
AOP
포커스
객체 간의 상호작용
관점의 핵심과 부가지점
사용 방식
객체를 독립적으로 사용하거나 부품처럼 결합
관점에 따라 공통 로직의 객체들을 하나로 모듈화
- AOP 예시 코드
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect // Aspect 생성
@Component // Aspect Class를 스프링 Bean으로 등록
public class CertainAspect{
@PointCut("execution(public * *(..))") // public 대상 PointCut선언
private void certainAdviceA(){
// 조건식 생성
}
@PointCut("within(com.xyz.myapp.trading..*)") // 특정 패키지 대상 PointCut선언
private void certainAdviceB(){
// 조건식 생성
}
@PointCut("certainAdviceA() && certainAdviceB()") // PointCut결합
private void mergeAdvice(){
// 조건식 생성
}
}
- 미리 정의된 포인트컷의 before/after/around에 advice실행
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect // Aspect 생성
public class CertainAspect{
@Before("com.xyz.myapp.certainPointCutMethod()")
private void certainAdvice(){
}
@AfterReturning("com.xyz.myapp.certainPointCutMethod()")
private void certainAdvice(){
}
@Around("com.xyz.myapp.certainPointCutMethod()")
private void certainAdvice(){
}
}
(3) Validation & Data Binding
- Validation
✅ Validation은 유효성 검증을 말하며, 주로 HTTP Request에서 잘못된 내용을 검증할 때 사용한다. - 데이터 검증 : 필수 데이터 / 문자열 길이 및 숫자형 데이터 범위 / 이메일 및 신용카드 번호 등 형식 확인 - 비즈니스 검증 : 서비스 정책에 따라 데이터 검증 ✅ Validation 방식 1) Java Bean Validation : dto클래스 맴버에 Annotaion(ex. @NotBlank, @Size, @Email...)을 붙이는 방식 2) Spring validator 인터페이스 구현을 통한 validation ✅ Validation 주의사항 : Validation을 멀리 두면 테스트 및 유지보수성이 떨어지므로 가능한 Annotaion방식을 쓰는 게 좋다. * 수업에서의 제안 : 1차로 dto에 Annotation방식 사용, 2차로 비즈니스 검증 후 실패 시 Custom Exception throw
- Data Binding
✅ Data Binding이란 요청 데이터를 특정 도메인 객체에 저장해 프로그램 Request에 담아주는 걸 말한다. >> ex. 어떤 데이터를 특정 dto객체로 변환하고 싶을 때 ✅ Data Binding 방식 1) Converter<S, T> interface를 구현한 클래스를 bean으로 등록해 사용 2) Formatter<T> interface를 구현한 클래스를 통해 특정 객체를 String, String를 특정 객체로 변환
(4) 스프링 표현 언어 (SpEL)
✅ 표현언어(EL)는 간단한 문법으로 필요한 데이터 및 설정값을 얻을 수 있게 하는 언어를 말한다.
* 이 부분은 차후에 더 정리할 예정.
| MVC
- MVC란? Model + View + Controller 의 약자
Model
View
Controller
내용
로직 내 이동 중인 데이터 (pf. 객체)
보여지는 화면
비즈니스 로직에 따라 처리하는 처리자
명칭
VO(value object), DTO(data transfer object)
-
-
- 최근에는 일반적으로 Request와 Response 모두 JSON 타입을 사용한다.
- Spring MVC의 흐름
출처:https://codingnotes.tistory.com/28
[1] Dispatcher Servlet이 요청 URL을 받아 HandlerMapping에 전달 * Dispatch : 파견 보내다. rf. 도서관의 사서(dispatcher)는 도서를 요청(request)받았고, 도서 코드 목록(HalderMapping)서 도서를 어떻게 찾을지 본다.
[2] HandlerMapping는 요청 URL에 맞는 Controller와 Method 정보를 반환 rf. 도서의 코드 목록에 따르면 해당 도서는 '역사' 카테고리 구역에 있단다.
[3] Dispatcher Servlet이 Handler Adapter에게 요청 처리를 위임 rf. 사서는 인턴에게 '역사' 카테고리 책장으로 이동해서, 가나다순으로 진열된 책들을 보면 된다고 알려준다.
[4] HandlerAdapter가 Controller와 Method를 실행 rf. 인턴이 찾으려는 도서의 코드와, 사서의 안내를 가지고 '역사' 구역에 가서 도서를 찾는다.
[5] Cotroller는 비즈니스 로직을 처리하고, 그 결과를 바탕으로 뷰(ex.JSP)에 전달할 객체를 Model 객체에 저장 rf. 인턴은 찾은 도서를 가져와 책수레에 담는다.
- 압축파일 해제한 뒤 IDE에 가져와 Build를 보면 아래와 같은데, 알아서 기본 세팅을 해준 걸 볼 수 있다.
/* 플러그인의 의존성(library) 관리 */
plugins {
id 'org.springframework.boot' version '2.7.3'
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
id 'java'
}
group = 'com.zerobase'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
/* 각종 의존성들(libraries)을 어떤 원격 저장소에서 받을 것인지 지정 */
repositories {
mavenCentral() // jcenter로 업로드 설정을 간소화할 수 있다.
}
/* 프로젝트 개발에 필요한 의존성들을 선언하는 곳 */
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
* Gradle이 Maven에 비해 최대 100배가 빠르다. (Maven vs Gradle 바로가기)
- Maven Project : xml 기반
- Gradle Project : 그루비 기반
[2] 인텔리j를 통해 바로 프로젝트 생성
[ 정리 ]
✅ 스프링 프로젝트를 생성할 때에는 spring.io 홈페이지나 IDE를 통해 생성할 수 있다.
✅ Bean 등록 : 각 클래스에 @Component를 쓰면 별도 등록이 필요 없다.
✅ DI Container에 Config 설정하기 : - config클래스에서 @confuration을 통해 각 클래스간 의존 관계를 DI(의존성 주입)하고, - @componentScan을 통해 IoC(제어의 역전) - 자동으로 의존 관계 설정 - 을 한다.
✅ Resource는 ApplicationContext 시작 시 ResourceLoader에 의해 자동 업로드되며, 별도 업로드 또한 가능하다.
✅ AOP는 관점 지향 프로그래밍으로 로깅, 트랜잭션, 인증 등의 반복적인 처리가 필요할 때 AspectJ lib를 통해 구현한다.
✅ Validation은 HTTP request에 대한 데이터 유효성 검증 또는 비즈니스 로직 검증을 하는 것으로, 1차적으로는 dto bean에 Annotation방식으로 데이터 유효성 검증 후, 2차적으로는 validator를 구현한 bean을 통해 비즈니스 로직을 검증하는 방식을 사용할 수 있다.
✅ Data-Binding이란 HTTP request로 받은 데이터를 특정 데이터 폼으로 변경하는 것을 말하는 것으로, Converter나 Formatter 인터페이스를 구현한 Bean을 가져와 사용하는 것으로 구현할 수 있다.
✅ SpEL이란, 스프링 표현언어를 말하는 것으로 간단한 문법을 통해 필요 데이터 및 설정값을 가져올 수 있는 언어를 말한다.