필드에 @Autowired 주입 처럼 편하게 사용하는 방법을 제공하는 lombok 라이브러리가 있다.
2. build.gradle 파일을 수정한다.
/build.gradle
plugins {
id 'org.springframework.boot' version '2.7.3'
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
id 'java'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
3. Plugins 를 확인한다.
Preferences -> plugins 검색 -> lombok 검색 -> installed 에 있는지 확인 ( 없으면 설치 )
4. Annotation processors 를 설정한다.
5. @Getter @Setter Annotation을 확인해본다.
package hello.core;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class HelloLombok {
private String name;
private int age;
public static void main(String[] args) {
HelloLombok helloLombok = new HelloLombok();
helloLombok.setName("teepo");
System.out.println(helloLombok.getName());
}
}
이렇게 따로 Getter, Setter 를 선언해주지 않아도 사용할 수 있다.
String에서도 유용한 상황이 있는데,
예를 들어 아래의 코드가 있을 때
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
// test
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
변수 선언에 final 을 붙이고 생성자를 Annotation으로 만들어 줄 수 있다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
// test
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
스프링 컨테이너는 지저분한 코드가 들어가지 않아도 된다.
3. 싱글톤 패턴의 주의점
여러 클라이언트가 하나의 같은 객체를 공유하기때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.
특정 클라이언트에 의존적인 필드가 있으면 안된다.
특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
가급적 읽기만 가능해야 한다.
변수를 따로 할당해주어서 쓰거나 해야지 직접 값을 변경하는 방법은 좋지 않다.
4. @Configration , 싱글톤
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
이런 식의 코드가 있을 때 일반적인 생각으로는 AppConfig 파일이 불러와질 때 memberRepository는 3번이 불러와져야 한다.
하지만 스프링이 싱글톤 패턴을 적용시켜서 한 번만 불러와진다.
5. 바이트코드 조작법
AppConfig 를 로그로 찍어보면 CGLIB이 붙은 것을 볼 수 있다.
자바 코드를 바이트 코드로 바꿔서 스프링 컨테이너에 등록을 하는 것을 알 수 있다.
바이트 코드를 비교해서 이미 등록이 된 bean 을 두 번 반환하지 않고 한 번만 반환하는 것을 알 수 있다.
이는 @Configuration Annotation 을 사용해야지만 바이트 코드를 사용한 싱글톤이 보장됨을 의미한다.
ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끈다. create 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다. 해보자.
3. JPA 엔티티 매핑
/com.example.demo/domain/Member
package com.example.demo.domain;
import javax.persistence.*;
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
4. JpaMemberRepository 생성
/repository/JpaMemberRepository
package com.example.demo.repository;
import com.example.demo.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member);
return null;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
}
5. 서비스 계층에 트랜잭션 추가
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class MemberService {
스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다. 만약 런타임 예외가 발생하면 롤백한다.
여기서 @ResponseBody 는 API 응답으로 Body 안에 원하는 데이터를 넣어주겠다는 뜻이다.
브라우저에서 접속해보면
return 값이 정상적으로 반환되는 것을 확인할 수 있다.
2. JSON return Controller
json 형식으로 반환할 땐 아래와 같이 할 수 있다.
@GetMapping("teepo-api")
@ResponseBody
public Teepo teepoApi(@RequestParam("name") String name) {
Teepo teepo = new Teepo();
teepo.setName(name);
return teepo;
}
static class Teepo {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
서버를 키고 확인해보면,
이와같이 json 형식으로 데이터를 받은 것을 확인할 수 있다.
3. Member 객체 생성
domain 폴더를 만들고 Member 에 대한 객체를 만들어보자.
com.example.demo/domain/Member
package com.example.demo.domain;
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.example.demo.repository;
import com.example.demo.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
//given
Member member = new Member();
member.setName("spring");
//when
repository.save(member);
//then
Member result = repository.findById(member.getId()).get();
assertThat(result).isEqualTo(member);
}
@Test
public void findByName() {
//given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//when
Member result = repository.findByName("spring1").get();
//then
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
//given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//when
List<Member> result = repository.findAll();
//then
assertThat(result.size()).isEqualTo(2);
}
}
// dotenv
import dotenv from 'dotenv'
dotenv.config();
// modules
import { By, until, WebDriver } from 'selenium-webdriver';
// logger
import { logger } from '../../config/winston';
// node-json-db
import { db } from '../../config/nodejsondb';
import {
getDriverHandler,
alertCloseAccept,
alertCloseDismiss,
promptCloseHandler,
addCookie,
getOneCookie,
getAllCookie,
deleteOneCookie,
deleteAllCookie,
fileRegister,
findElementById,
findElementByName,
findElementByXpath,
findElementByCss,
JqChangeValueByID,
JqRemoveAttribute,
naviGet,
naviBack,
naviForward,
naviRefresh,
popupClose
} from '../../modules';
describe('GET /one', () => {
// 웹드라이버 설정
let driver : WebDriver;
beforeAll(async () => {
driver = await getDriverHandler();
})
afterAll(async () => {
await driver.quit()
})
test('url 잘 뜨는지 확인', async () => {
try {
// 브라우저에 접속
await driver.get('https://typo.tistory.com/');
// 현재 주소 가져오기
const text = await driver.getCurrentUrl();
// test
expect(text).toEqual('https://typo.tistory.com/')
}
catch(Err) {
console.log(Err)
logger.debug(Err)
throw Error;
}
})
})
/tests/middlewares/http.test.ts
import express, { Request, Response, NextFunction } from 'express';
import {
loggerInfo,
loggerError,
loggerHttp,
loggerDebug,
} from '../../config/winston'
import { httpLoggingMiddleware } from '../../middlewares';
describe('MiddleWare http', () => {
let mockRequest : Partial<Request>;
let mockResponse : Partial<Response>;
let nextFunction: NextFunction = jest.fn();
beforeEach(()=> {
mockRequest = {};
mockResponse = {
json: jest.fn()
};
});
test('http 로깅 미들웨어 테스트', async () => {
try {
await httpLoggingMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);
// nextFunction 까지 잘되는지 확인
expect(nextFunction).toBeCalledTimes(1);
}
catch (Err) {
loggerDebug.info(JSON.stringify(Err))
}
})
})
reqest, response, next 를 mock으로 만들어 테스트해준다.
/tests/middlewares/timeInterval.ts
import express, { Request, Response, NextFunction } from 'express';
import {
loggerInfo,
loggerError,
loggerHttp,
loggerDebug,
} from '../../config/winston'
import { timeIntervalMiddleware } from '../../middlewares';
describe('MiddleWare timeInterval', () => {
let mockRequest : Partial<Request>;
let mockResponse : Partial<Response>;
let nextFunction: NextFunction = jest.fn();
beforeEach(()=> {
mockRequest = { originalUrl : '/one' };
mockResponse = {
json: jest.fn()
};
});
test('timeInterval 로깅 미들웨어 테스트', async () => {
try {
await timeIntervalMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);
// nextFunction 까지 잘되는지 확인
expect(nextFunction).toBeCalledTimes(1);
}
catch (Err) {
loggerDebug.info(JSON.stringify(Err))
}
})
})
/tests/modules/db/expireHandler.ts
import express, { Request, Response, NextFunction } from 'express';
// node-json-db
import { db } from '../../../config/nodejsondb';
// logger
import { loggerDebug } from '../../../config/winston';
import { expireHandler } from '../../../modules';
describe('Module expireHandler', () => {
let dbPath : string;
beforeEach(()=> {
dbPath = '/one'
});
test('30분 지난 데이터 삭제 및 확인', async () => {
try {
let nowTime = new Date(); // 현재 시간
// 30분 지난 데이터 삭제
await expireHandler(dbPath);
// 데이터 베이스 안 데이터들
const datalist = await db.getData(dbPath);
let index = 0; // 기준점이 되는 index
// 데이터 매핑 및 시간 차이 계산해서 30분 지난 데이터는 삭제
await datalist.map(async (item : any, index2: number) => {
let beforeTime = new Date(item.date); // 데이터들 저장된 시간
let diff = (nowTime.getTime() - beforeTime.getTime()) / (1000*60); // 데이터들 시간 차이
// 저장된 지 30분이 지나면 index 체크
if(diff > 30) {
index = index2;
}
})
// ----- 30분 지난 데이터 없는지 확인 -----
expect(index).toEqual(0);
}
catch(Err) {
console.log("Err : ",Err)
loggerDebug.info(JSON.stringify(Err))
}
})
})
/tests/modules/selenium/alertHandler.test.ts
// dotenv
import dotenv from 'dotenv'
dotenv.config();
// modules
import { By, until, WebDriver } from 'selenium-webdriver';
// logger
import {
loggerDebug,
} from '../../../config/winston';
import {
getDriverHandler,
findElementById,
} from '../../../modules';
// window type 선언
declare const window: typeof globalThis;
describe('Module alertHandler', () => {
// 웹드라이버 설정
let driver : WebDriver;
beforeEach(async () => {
driver = await getDriverHandler();
await driver.get('https://testpages.herokuapp.com/styled/alerts/alert-test.html')
})
afterEach(async () => {
await driver.quit()
})
test('alert 테스트 ( alert 텍스트가 잘 나오는지 )', async () => {
try {
await (await findElementById(driver,'alertexamples')).click();
// alert 창 뜰 때까지 기다림
await driver.wait(until.alertIsPresent());
// alert 로 드라이버 이동
let alert = await driver.switchTo().alert();
// alertText
let alertText = await alert.getText();
// 확인버튼 클릭
await alert.accept();
// 다시 원래 컨텐츠로 드라이버 이동
await driver.switchTo().defaultContent();
expect(alertText).toEqual('I am an alert box!');
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
})
test('prompt 테스트', async () => {
try {
await (await findElementById(driver,'promptexample')).click();
// alert 창 뜰 때까지 기다림
await driver.wait(until.alertIsPresent());
// alert 로 드라이버 이동
let alert = await driver.switchTo().alert();
// alert text 사용하는 곳
let alertText = await alert.getText();
// 확인버튼 클릭
await alert.accept();
// 다시 원래 컨텐츠로 드라이버 이동
await driver.switchTo().defaultContent();
expect(alertText).toEqual('I prompt you');
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
})
})
/tests/modules/selenium/cookieHandler.test.ts
// dotenv
import dotenv from 'dotenv'
dotenv.config();
// modules
import { By, until, WebDriver } from 'selenium-webdriver';
// logger
import {
loggerDebug,
} from '../../../config/winston';
import {
getDriverHandler,
addCookie,
getOneCookie,
getAllCookie,
deleteOneCookie,
findElementById,
findElementByName,
} from '../../../modules';
describe('Module cookieHandler', () => {
// 웹드라이버 설정
let driver : WebDriver;
beforeEach(async () => {
driver = await getDriverHandler();
await driver.get('https://testpages.herokuapp.com/styled/cookies/adminlogin.html');
// 로그인 및 쿠키생성
await (await findElementByName(driver,'username')).sendKeys('Admin');
await (await findElementByName(driver,'password')).sendKeys('AdminPass');
await (await findElementById(driver,'login')).click();
},15000)
afterEach(async () => {
await driver.quit()
})
test('쿠키 추가하기', async () => {
try {
// 쿠키추가
await addCookie(driver,{name : "name", value : "teepo"});
// 쿠키 가져오기
const cookies = (await driver.manage().getCookie('name')).value;
// 쿠키 확인
expect(cookies).toEqual('teepo');
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
})
test('쿠키 하나 가져오기', async () => {
try {
// 쿠키 가져오기
const result = await getOneCookie(driver,'loggedin');
// 쿠키 확인
expect(result.value).toEqual('Admin');
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
})
test('쿠키 전부 가져오기', async () => {
try {
// 쿠키 가져오기
const result = await getAllCookie(driver);
// 쿠키 확인
expect(result[0].value).toEqual('Admin');
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
})
test('쿠키 하나 지우기', async () => {
try {
// 쿠키 삭제하기
await deleteOneCookie(driver,'loggedin');
// 쿠키 확인
const result = await getAllCookie(driver);
// 쿠키가 없어졌는지 확인
expect(result.length).toEqual(0);
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
})
test('쿠키 전부 지우기', async () => {
try {
// 쿠키 삭제하기
await deleteOneCookie(driver,'loggedin');
// 쿠키 확인
const result = await getAllCookie(driver);
// 쿠키가 없어졌는지 확인
expect(result.length).toEqual(0);
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
})
})
// dotenv
import dotenv from 'dotenv'
dotenv.config();
// modules
import { By, until, WebDriver } from 'selenium-webdriver';
// logger
import {
loggerDebug,
} from '../../../config/winston';
import {
getDriverHandler,
findElementById,
findElementsById,
findElementByName,
findElementByXpath,
findElementByClass,
findElementsByName,
findElementsByXpath,
findElementsByClass
} from '../../../modules';
describe('Module findElementHandler', () => {
// 웹드라이버 설정
let driver : WebDriver;
beforeAll(async () => {
driver = await getDriverHandler();
driver.get('https://testpages.herokuapp.com/styled/find-by-playground-test.html')
})
afterAll(async () => {
await driver.quit()
})
test('findElementById', async () => {
try {
let text = await (await findElementById(driver,'p1')).getText();
expect(text).toEqual('This is a paragraph text')
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
});
test('findElementByName', async () => {
try {
let text = await (await findElementByName(driver,'pName1')).getText();
expect(text).toEqual('This is a paragraph text')
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
});
test('findElementByXpath', async () => {
try {
let text = await (await findElementByXpath(driver,'//*[@id="p1"]')).getText();
expect(text).toEqual('This is a paragraph text')
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
});
test('findElementByClass', async () => {
try {
let text = await (await findElementByClass(driver,'explanation')).getText();
expect(text).toEqual('This is a set of nested elements. There are various ways to locate each of the elements. Challenge yourself to find as many ways of locating elements as possible.')
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
});
})
describe('Module findElementsHandler', () => {
// 웹드라이버 설정
let driver : WebDriver;
beforeAll(async () => {
driver = await getDriverHandler();
await driver.get('https://testpages.herokuapp.com/styled/find-by-playground-test.html')
})
afterAll(async () => {
await driver.close()
})
test('findElementsById', async () => {
try {
let text = await (await findElementsById(driver,'p1'))[0].getText();
expect(text).toEqual('This is a paragraph text')
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
});
test('findElementsByName', async () => {
try {
let text = await (await findElementsByName(driver,'pName1'))[0].getText();
expect(text).toEqual('This is a paragraph text')
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
});
test('findElementsByXpath', async () => {
try {
let text = await (await findElementsByXpath(driver,'//*[@id="p1"]'))[0].getText();
expect(text).toEqual('This is a paragraph text')
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
});
test('findElementsByClass', async () => {
try {
let text = await (await findElementsByClass(driver,'explanation'))[0].getText();
expect(text).toEqual('This is a set of nested elements. There are various ways to locate each of the elements. Challenge yourself to find as many ways of locating elements as possible.');
}
catch(Err) {
loggerDebug.info(JSON.stringify(Err))
}
});
})