클라이언트 요청을 처리하는 메서드인 service()를 재정의하여야 하는 일반적인 인터페이스. HTTP를 제외한 별도의 독립적인 프로토콜을 이용하는 서블릿을 만들 때 사용한다.
public class ExampleServlet extend GenericServlet{
// 재정의
@Override
public void service(ServletRequest req, SetvletResponse res)
throws IOException, ServletException {
System.out.println("Service");
}
}
HttpServlet?
HTTP 프로토콜에 대한 요청을 처리하는 Service() 메서드들을 제공하는 구현체이다.
- ServletRequest, Response를 HttpServlet 형식으로 Casting 하는 Service() - 요청 Method 타입에 따라 별도의 로직을 실행하는 service()가 존재한다.
// 해당 메서드는 요청을 casting하는 service()에서 호출한다.
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 해당 요청의 메서드 값
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
HttpServletBean?
Servlet init()과 관련하여 초기에 설정되는 값들을 Bean 속성으로 매핑하는 확장 클래스이다.
Spring Framework에서 사용되는 모든 Servlet에 대해 적용된다.
// 구성 매개 변수를 매핑한 뒤 초기화를 진행한다.
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
initServletBean();
}
HttpServlet의 기본 동작을 상속받지만, 모든 요청 처리에 대해서 Sub Class에게 위임한다.
(FrameworkServlet, DispatcherServlet은 해당 구현체를 상속받는다.)
FrameworkServlet?
Spring Framework에서 사용되는 Servlet 기능을 위한 구현체이다.
WebApplicationContext를 통해 Servlet 인스턴스가 관리되게끔 지원한다.
해당 구현체로는 doService()를 처리할 수 없으며, 해당 메서드 처리를 위해서는 하위 구현체를 구현하여야 한다. (DispatcherServlet은 해당 메서드를 구현하고 있다.)
protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
throws Exception;
/**
* Delegate GET requests to processRequest/doService.
*
Will also be invoked by HttpServlet's default implementation of {@code doHead},
* with a {@code NoBodyResponse} that just captures the content length.
* @see #doService
* @see #doHead
*/
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
/**
* Delegate POST requests to {@link #processRequest}.
* @see #doService
*/
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
코드를 보게 되면 doService()는 추상 메서드로, doPost, doGet 등은 별도 처리를 거치고 위임을 하는 형식을 지닌다.
Default Context는 XMLApplicationContext이다.
(HttpServletBean이 xml기반이기 때문일까?)
DispatcherServlet?
Spring MVC는 하나의 Servlet을 통해 요청을 처리하고 매핑하는 형식의 구조를 지닌다.
비즈니스 로직에 대하여서는 Adapter를 통해 Mapping 된 컨트롤러에게 위임하며, 설정 값에 따라 Resolver를 선택하거나 컨버터를 통해 랜더링을 거치고 결과를 반환한다.
해당 글은 Notion에 정리되어 있던 Somaeja 프로젝트 관련 정리 글 중 하나입니다.
이번 프로젝트를 진행할 때 Post Service에 Transcation Read only 설정을 적용하게 되었었다.
단순히 read only를 사용하면 read-write 보다 성능이 더 좋다고 들었기 때문이다. 그러지 마...
하지만 해당 설정에 대하여서 DB마다 동작 방식이 다르다고 하여, 많이 사용되는 DB들을 기준으로 조사해보았다.
Oracle
Read Only 트랜잭션을 이용할 경우 이 트랜잭션이 시작되기 이전에 커밋된 데이터만 접근할 수 있으며, 트랜잭션 실행되는 동안 커밋되는 데이터는 결과에 반영되지 않는다.
해당 트랜잭션 시에 지원하는 DML은 SELECT(조회) 구문뿐이다.
→ 해당 트랜잭션 내에서 일관적인 데이터를 얻도록(데이터 일관성) 보장한다. 즉 성능 이점만을 위함이 아니다.
PostgreSQL
해당 트랜잭션을 이용할 경우 SELECT를 제외한 DDL, DML, DCL은 동작하지 않는다.
Postgresql에서는 읽기 동작을 가정할 경우, Read/Write 속성과 성능 차이를 가지지 않으며 (해당 부분의 최적화 X) Deffered 속성(행 단위가 아닌 트랜잭션 단위의 제약조건 검증 처리)을 적용하였을 때, SERIALIZABLE 이거나 READ ONLY를 사용하게 되어 내부의 튜플이 수정되지 않는 안전한 동작을 하게끔 지원하는 용도이다.
→ Postgre의 Read Only 설정도 성능 이점이 아닌 동시성 제어를 위함이다.
→ Postgre는 Read Only 설정 시 트랜잭션 ID를 일반적인 ID가 아닌 가상 ID로 제공하기에 실제로 제공되는 트랜잭션 ID가 수가 줄어들어 성능이 개선될 수도 있다.
Postgresql의 튜플(Tuple)?
MVCC를 지원하기 위해 사용되는 기능으로 PostgreSQL은 트랜잭션 처리 시 기존에 가지고 있는 데이터 로우를 복제하여 기존 로우를 사용자들이 조회하는 용도로, 복제한 row를 트랜잭션 처리 용도 (데이터)로 사용하게 되는데 이러한 원본, 복제본에 버전을 명시할 때 사용되는 것이 튜플이다.
삭제, 롤백, 업데이트 시에는 생성된 튜플들에 대하여서 Dead라는 마킹이 적용되며 (실제 데이터가 삭제되지 않는다.) VACUUM FULL를 통하여 해당 데이터들을 파기하게 된다.
MySQL
해당 트랜잭션을 이용할 경우 SELECT 문에 대해서만 기능을 지원하며, Transaction ID 설정에 대한 오버헤드를 해결할 수 있다. Read Only 트랜잭션에 대해서는 ID가 부여되지 않는다.
추가적으로 세션 별로 임시 테이블을 변경하거나 Lock 쿼리를 실행할 수 있는데, 이는 다른 트랜잭션(Read Write 혹은 다른 Read Only) 들이 가시 할 수 없는 범위이기 때문이다.
Oracle과 마찬가지로 별도의 스냅샷을 통해 데이터를 조회하기 때문에, 데이터 일관성을 보장할 수 있다.
→ 트랜잭션 ID 설정에 대한 오버헤드를 해결하고, 스냅샷을 통해 데이터 일관성을 보장한다.
추가) Spring의 Read Only 설정 방식?
Oracle은 9i, 10g 버전에서 Connection.setReadOnly를 통해 해당 트랜잭션을 지원하였다.
하지만 이후 버전에서는 11, 12c 드라이버를 사용하는 경우에 Connection.setReadOnly(true)를 설정하더라도 Read only 트랜잭션이 설정, 동작하지 않았었다.
이것을 해결하기 위해 DataSourceTransactionManager에 repareTransactionConnection Template method가 추가되었으며, setEnforceReadOnly 설정을 true로 설정하면 prepareTransactionalConnection를 통한 DB와 커넥션 연결 시에 SET TRANSACTION READ ONLY 구문을 보냄으로써 Read Only Transaction을 사용할 수 있게 되었다.해당 설정은 스프링 4.3.7 이후에 추가되었다. (Oracle, MySQL, Postgre에 적용 가능)
// DataSourceTransactionManager.java
private boolean enforceReadOnly = false;
/**
* Specify whether to enforce the read-only nature of a transaction
* (as indicated by {@link TransactionDefinition#isReadOnly()}
* through an explicit statement on the transactional connection:
* "SET TRANSACTION READ ONLY" as understood by Oracle, MySQL and Postgres.
* <p>The exact treatment, including any SQL statement executed on the connection,
* can be customized through {@link #prepareTransactionalConnection}.
* <p>This mode of read-only handling goes beyond the {@link Connection#setReadOnly}
* hint that Spring applies by default. In contrast to that standard JDBC hint,
* "SET TRANSACTION READ ONLY" enforces an isolation-level-like connection mode
* where data manipulation statements are strictly disallowed. Also, on Oracle,
* this read-only mode provides read consistency for the entire transaction.
* <p>Note that older Oracle JDBC drivers (9i, 10g) used to enforce this read-only
* mode even for {@code Connection.setReadOnly(true}. However, with recent drivers,
* this strong enforcement needs to be applied explicitly, e.g. through this flag.
* @since 4.3.7 * @see #prepareTransactionalConnection
*/
public void setEnforceReadOnly(boolean enforceReadOnly) {
this.enforceReadOnly = enforceReadOnly;
}
/**
* Return whether to enforce the read-only nature of a transaction
* through an explicit statement on the transactional connection.
* @since 4.3.7
* @see #setEnforceReadOnly
*/
public boolean isEnforceReadOnly() {
return this.enforceReadOnly;
}
/**
* Prepare the transactional {@code Connection} right after transaction begin.
* <p>The default implementation executes a "SET TRANSACTION READ ONLY" statement
* if the {@link #setEnforceReadOnly "enforceReadOnly"} flag is set to {@code true}
* and the transaction definition indicates a read-only transaction.
* <p>The "SET TRANSACTION READ ONLY" is understood by Oracle, MySQL and Postgres
* and may work with other databases as well. If you'd like to adapt this treatment,
* override this method accordingly.
* @param con the transactional JDBC Connection
* @param definition the current transaction definition
* @throws SQLException if thrown by JDBC API
* @since 4.3.7
* @see #setEnforceReadOnly
*/
protected void prepareTransactionalConnection(Connection con, TransactionDefinition definition) throws SQLException {
if (isEnforceReadOnly() && definition.isReadOnly()) {
try (Statement stmt = con.createStatement()) {
stmt.executeUpdate("SET TRANSACTION READ ONLY");
}
}
}
Read only Transaction을 사용하는 일반적인 이유
해당 설정을 사용한 트랜잭션은 데이터(테이블, 칼럼)에 대하여 Lock을 적용할 필요가 없고 접근할 수 있는 데이터가 (스냅샷, 튜플 등) 변경되지 않기 때문에, 일관적인 데이터를 읽어오고 제공할 수 있다.
해당 속성의 경우 일반적으로 트랜잭션 ID를 부여하지 않아도 되기에 불필요한 ID 설정에 대한 오버헤드가 발생하지 않기 때문에 성능의 이점을 볼 수 있다.
@Configuration
public class ETagHeaderFilter {
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> shallowEtagHeaderFilter() {
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean = new FilterRegistrationBean<>( new ShallowEtagHeaderFilter());
filterRegistrationBean.addUrlPatterns("/api/notices");
return filterRegistrationBean;
}
}
@Bean 메서드는 스프링 컨테이너에 의해 관리되는 Configuration Proxy Bean을 통해 호출되는 일종의 펙토리 메서드입니다. 일반적으로 위와 같이 FilterRegistrationBean과 같은 특정 객체에 값과 설정을 제공하고, 그 결과 해당 Bean 혹은 빌더의 결과를 전달하게 됩니다.
ETag가 설정 되었는지 확인해보기
이후 사용될 MockMvc를 언급하자면, 이 객체는 MVC 테스트에 필요한 객체로 실제 웹 애플리케이션을 구동시키지 않고 클라이언트의 역할을 하여 MockHttpServletRequest, Response를 보내고 받아 검증할 수 있습니다.
이를 통해 작성된 테스트 코드를 통해 동작 여부를 확인할 수 있습니다.
기본적인 작성 방식은
mockMvc.perform({MockHttpServletRequestBuilder.get, delete, post...}({URI 정보})
.content({요청 바디에 담을 정보})
.contentType({요청 바디의 타입}))
.andDo(print()) // 요청, 응답 결과를 콘솔에 출력
.andExpect(status().isCreated()) // Http 상태, 바디 값, 헤더 등 존재 여부 등 응답 결과를 검증
; //.andExpect(jsonPath()); JSON 데이터 검증
MockHttpServletResponse:
Status = 304
Error message = null
Headers = [Content-Type:"application/json", ETag:""0e3344bf1e63fc52fbe7ce711a2f5014f""]
Content type = application/json
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
서버의 리소스를 업데이트 한 뒤 요청을 한다면?
mockMvc.perform(post("/api/notices")
.content(objectMapper.writeValueAsString(createNoticeDto))
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isCreated());
// 이전 요청에서 보낸 ETag 요청을 전송
mockMvc.perform(get("/api/notices")
.header("If-None-Match", etag)
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk());
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json", ETag:""0649d2b6caf906a380376fe9258f0a7ae"", Content-Length:"442"]
Content type = application/json
Body = [{"id":3,"author":"author","title":"title","content":"content","createDate":"2021-03-07T17:27:19.557702","modifyDate":"2021-03-07T17:27:19.557702"},{"id":2,"author":"author","title":"title","content":"content","createDate":"2021-03-07T17:27:19.504702","modifyDate":"2021-03-07T17:27:19.504702"},{"id":1,"author":"author","title":"title","content":"content","createDate":"2021-03-07T17:27:19.423703","modifyDate":"2021-03-07T17:27:19.423703"}]
Forwarded URL = null
Redirected URL = null
Cookies = []
public class MapStructExample$DataMapperImpl implements DataMapper {
public MapStructExample$DataMapperImpl() {
}
public SampleDto toDto(SampleEntity entity) {
if (entity == null) {
return null;
} else {
SampleDtoBuilder sampleDto = SampleDto.builder();
List<String> list = entity.getSampleInfo();
if (list != null) {
sampleDto.infos(new ArrayList(list));
}
sampleDto.name(entity.getName());
sampleDto.email(entity.getEmail());
sampleDto.age(entity.getAge());
return sampleDto.build();
}
}
public SampleEntity toEntity(SampleDto dto) {
if (dto == null) {
return null;
} else {
SampleEntityBuilder sampleEntity = SampleEntity.builder();
List<String> list = dto.getInfos();
if (list != null) {
sampleEntity.sampleInfo(new ArrayList(list));
}
sampleEntity.name(dto.getName());
sampleEntity.email(dto.getEmail());
sampleEntity.age(dto.getAge());
return sampleEntity.build();
}
}
}
해당 방식의 장, 단점
장점
간결한 코드 작성이 가능하다.
객체 필드의 변경 사항이 다른 로직에 영향을 주지 않는다.
컴파일 시점에 코드를 생성하면서 타입이나 매핑이 불가능한 상태 등의 문제가 발생한 경우 컴파일 에러를 발생시킨다. 이는 상대적으로 런타임에서 안전성을 보장한다.
앞서 보았던 자바 코드 매핑 방식과 같은 수준의 성능을 가진다.
단점
전혀 다른 형태의 필드 매핑을 시도하는 경우 제공되는 기능으로 해결 가능한 경우가 많으나, Mapping 로직이 매우 복잡해진다.
변경 불가능한 필드에 대한 매핑을 제공하지 못한다. (final 필드 - Constructor 주입)
Lombok Library와 충돌이 발생할 수 있다. (실제로는 Lombok annotation processor가 getter나 builder 등을 만들기 전에 mapstruct annotation processor가 동작하여 매핑할 수 있는 방법을 찾지 못해 발생하는 문제이다. )
ModelMapper
MapStruct와 같이 객체 간의 변환을 위해 사용되는 라이브러리이며, MapStruct와 다른 점은 런타임 시점에 Reflection API를 사용하여 객체를 매핑한다는 것이다.
ModelMapper modelMapper = new ModelMapper();
public void dtoToEntity() {
SampleDto dto = SampleDto.builder()
.name("dto")
.email("...@hello")
.age(20L)
.infos(new ArrayList<>(Collections.singleton("aaa")))
.build();
SampleEntity entity = modelMapper.map(dto, SampleEntity.class);
System.out.println(entity);
}
public void EntityToDto() {
SampleEntity entity = SampleEntity.builder()
.id(1L)
.name("lob")
.email("...@test")
.age(20L)
.value("value")
.sampleInfo(new ArrayList<>(Collections.singleton("aaa")))
.build();
// 이름이 다른 필드 매핑을 위해 PropertyMap 선언
PropertyMap<SampleEntity, SampleDto> sampleMap = new PropertyMap<>() {
@Override
protected void configure() {
map().setInfos(source.getSampleInfo());
}
};
// ModelMapper 구성 정보에 PropertyMap 추가
modelMapper.addMappings(sampleMap);
// createTypeMap() 을 사용할 수도 있다.
//modelMapper.createTypeMap(SampleEntity.class, SampleDto.class)
// .addMapping(SampleEntity::getSampleInfo, SampleDto::setInfos);
SampleDto dto = modelMapper.map(entity, SampleDto.class);
System.out.println(dto);
}
--------
SampleEntity(id=null, name=dto, email=...@hello, age=20, sampleInfo=null, value=null)
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access using Lookup on org.modelmapper.internal.ProxyFactory (file:/C:/Users/serrl/.gradle/caches/modules-2/files-2.1/org.modelmapper/modelmapper/2.3.9/8bb9110f8df3fbd6c1c2e4b69f7c6add737888e7/modelmapper-2.3.9.jar) to interface java.util.List
WARNING: Please consider reporting this to the maintainers of org.modelmapper.internal.ProxyFactory
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
SampleDto(name=lob, email=...@test, infos=[aaa], age=20)
해당 방식의 장, 단점
장점
간결한 코드 작성이 가능하다.
일반적으로 필드 변경 사항에 대해서 고려하지 않아도 된다.
Lombok 라이브러리와 충돌없이 같이 사용할 수 있다. (런타임에서 객체를 분석하고 매핑하기 때문에)
단점
(처음 학습하시는 분들의 오해를 일으킬 수 있어보여서 수정합니다. ㅎㅎ)
컴파일 시점에 생성된 코드를 기반으로 최적화하여 동작하는 위의 방식들과 달리 해당 방식은컴파일러에 의해 최적화 되지 않으며, 최초 작업 시 캐싱되지 않고(이후 작업에는 만들어진 Map을 재사용합니다.), 매칭 및 매핑 로직에서 Refliection API를 사용해 객체 필드 정보를 추출하고 Map을 만든 다음 들어온 인자와 매칭시켜주고 Map의 정보를 기준으로 값을 매핑해주는 방식이기에 다른 방식보다 오버헤드가 많아 상대적으로 성능이 좋지 않은 것이다.
Reflection API를 사용한다는 이유만으로 느린 것이 아니다.
바이트코드 생성 방식을 이용하기에 문제 원인 발견과 디버깅이 어렵다. 특정 필드의 변경으로 인한 매핑 누락 문제가 발생하였을 때 이를 인지하기 힘들다.
일반적으로 setter를 사용한다. (개인적으로 Setter를 통해 모든 필드를 열어놓는 행위를 싫어한다.)
불변 객체나 Setter를 사용하지 않기 위해 설정하는 방법으로는 별도로 TypeMap과 provider를 정의하고 내부에서 생성자를 이용하는 방법, Converter를 구현하는 방법 (생성자를 로직에 활용하는 것은 Provider와 동일), ModelMapper의 필드 접근 수준을 private로 설정하는 방법이 있다.
// 1
modelMapper.createTypeMap(SampleEntity.class, SampleDto.class)
.setProvider( new Provider<SampleDto>() {
public SampleDto get(ProvisionRequest<SampleDto> request) {
Source source = Source.class.cast(request.getSource());
return new SampleDto(source.name, source.address);
}
});
// 2
Converter<SampleEntity, SampleDto> SampleConverter = () {
@Override
public SampleDto convert(MappingContext<SampleEntity, SampleDto> context) {
SampleEntity entity = context.getSource();
return new SampleDto(entity.getName(), entity.getAddress());
}
};
// 3
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setFieldMatchingEnabled(true)
.setFieldAccessLevel(Configuration.AccessLevel.PRIVATE);
Autowired처럼 빈 타입을 기준으로 찾아서 주입하는 방식 (타입 → @Qualifier → 이름)
사용할 수 있는 위치
필드
세터
생성자
메서
해당 타입의 빈이 여러 개인 경우
@Primary
@Order
@Qualifier
Bean 이름
Spring DI 방식
Constructor Injection
Bean을 등록하기 위한 Component Scan 중에 해당 Class 정보를 확인하게 된다.
Spring 4.2까지는 Autowired를 붙여야 한다.
해당 방식의 특징
객체 생성 시 호출되는 생성자를 통해 1번만 주입되는 것을 보장한다.
불변적이며, 우선적으로 주입된다.
Spring에서는 필수적인 의존성 주입에 대하여서 생성자 주입을 권장한다.
@Service
public class UserService {
private final UserRepository userRepository;
// 해당 클래스가 다른 생성자를 가지지 않는다면 생략할 수 있다. Spring 4.3+
// 그리고 추가적으로 Configuration 클래스에도 생성자 주입을 할 수 있게 되었다.
@Autowired
public UserAccountService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// Logic...
}
해당 방식의 장점
단위 테스트가 용이하다.
Bean을 생성하는 동안 다른 Bean을 초기화할 수 있다. 별도의 값을 저장한다던지..
클래스 레벨에서 불변한 객체 주입이 가능하다.만약에 주입 대상이 되는 Bean들이 없거나, 오류로 인해 주입되지 않을 경우 Checked Exception을 통하여 알아차릴 수 있다. = NPE를 방지한다.
final Keyword와 생성 시점의 주입을 통해 갑작스러운 변경에 의한 문제를 해결하게 된다.
쉽게 불변 객체를 만들어낼 수 있다.
안정적인 상태를 유지할 수 있다. → 모든 의존성이 주입되거나 인스턴스화를 실패한다.
Reflection API를 사용하지 않는다.주입 시에 Spring 상에서 최소의 자원을 사용한다.
생성자를 사용하여 인스턴스를 생성하는 것이 OOP 관점에서 자연스럽다.
해당 방식의 단점
해당 Bean이 선택적인 종속성을 가져야 하는 경우가변적인 상태
Setter Injection을 사용하자. 생성자 주입과 설정자 주입은 결합이 가능하다.
이것들은 단점일까?
해당 Bean이 적은 가짓수의 의존성을 가질 경우
해당 Bean의 필수 의존성이 적을 경우 생성자 주입 코드는 "장황해 보일 수도" 있다.
해당 Bean이 아주 많은 가짓수의 의존성을 가질 경우Ugly Constructor
종속성이 추가될 때마다 생성자의 매개 변수에 추가해야 하므로 생성자를 통해 과도한 결합을 노출하게 된다.
추악한 생성자? ( Ugly Constructor? )
클래스가 수많은 필드(의존성)를 가지고 작성되어 있다면, 생성자도 복잡해질 수밖에 없다.
// 점층적 생성자 패턴
public class Employee {
// 필수적인 속성, 의존성
private final String firstname;
private final String lastname;
// 선택적인 속성, 의존성
private String email;
private String address;
private String phone;
// Default Constructor 는 필수적인 의존성(final) 을 포함하여야한다.
public Employee(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public Employee(String email, String address, String phone) {
this.email = email;
this.address = address;
this.phone = phone;
}
public Employee(String firstname, String lastname, String email, String address, String phone) {
this.firstname = firstname;
this.lastname = lastname;
this.email = email;
this.address = address;
this.phone = phone;
}
}
사실 가정하는 상황에서는 이것보다 더 많은 인자를 가지고 있을 것이다. 그럴 경우에는 생성자가 더욱 복잡해질 것이고, 객체의 코드를 작성하거나 파악하는 것에 어려움을 겪을 것이다.
Spring의 경우에는 수많은 Service 혹은 Repository의 생성자 주입을 말할 수 있다.
사실 이러한 생성자가 만들어진다는 것은 그 객체의 책임이 혼재되어 있거나 과중한 책임을 지니고 있음을 의미하는 것이다.
그렇기에 이것은 생성자 주입의 문제로만 치부하기보다는 설계의 문제임을 생각하는 것이 좋다고 생각한다.
Setter Injection
생성된 Bean 정보들 중에서 @Autowired가 붙은 Setter를 찾아 호출하며 Bean을 주입한다.
Method Injection도 비슷하게 동작한다.
해당 방식의 특징
자바 빈 규약(setter)을 이용한 의존성 주입 방식이다.
Spring에서는 변경 가능성 있는, 선택적인 종속성에 대해서 설정자 주입을 권장한다.
@Service
public class UserService {
private final UserRepository userRepository;
// 특별한 요청에 경우 필요하다고 가정.
private GuestRepository guestRepository;
public UserAccountService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setGuestRepository(GuestRepository guestRepository) {
this.guestRepository = guestRepository;
}
// Logic...
}
해당 방식의 장점
선택적인 종속성에 대하여서 별도의 설정자를 만들고 유연한 제공이 가능하다.
해당 방식의 단점
의존성이 주입되는 시점이 많아질 수 있기에 가독성이 떨어지게 되고, 상대적으로 유지보수 능력을 떨어트린다.
설정자가 호출될 때마다 객체가 새로 주입됨으로 참조에 대한 불변을 유지할 수 없다.
Null이 주입될 수 있고, 새로운 객체의 반환으로 기존 작업 값을 잃을 수 있다.
설정자를 통해 클래스 내부의 의존성 부분을 노출함으로써 캡슐화를 위반하게 된다.
객체 생성 시점에는 순환 종속성이 발견되지 않으나, 비즈니스 로직이 진행되는 도중에 해당 문제가 발생할 수 있다.
Field Injection
필드 주입을 위해 Reflection API를 이용하여 정보를 찾고 접근 제어자 등을 조작한다.
@Service
public class UserService {
// 간단한 주입방식!
@Autowired private UserRepository userRepository;
// Logic...
}
해당 방식의 장점
모든 주입 방식 중 가독성 측면에서 가장 간결하게 사용할 수 있다.생성자와 수정자 코드 등
필드 주입을 선택하는 이유 중 하나는 보일러 플레이트 코드를 작성하지 않기 위함이다.
많은 필드를 갖는 것이 불가피하고 추악한 생성자가 문제가 되는 상황에서는 필드 주입을 사용하는 것이 "나을 수도" 있다. 별도의 Null 체크가 필요하고.. 이건 설계 문제다!
그런 상황에서 필드 주입을 선택하는 것이 최선인지는 의문이 든다.
사용할 것이라면..
Application 코드와 관계없는 테스트 코드에 사용 (Mock 객체 주입 등)
스프링 설정을 목적으로 하는 @Configuration 클래스에서만 사용
해당 방식의 단점
Private 한 필드에 의존성을 주입할 수 있는 유일한 방법은 스프링 컨테이너가 클래스를 인스턴스 화하고 Reflection API를 사용하여 주입하는 것이다.
이러한 주입 방식은 생성자, 설정자 주입 방식보다 많은 자원을 소모한다.
Spring의 DI에 의존적인 코드를 작성하게 된다. 순수한 자바 코드가 아니다.
의존성 주입을 실패하게 된 경우 Exception이 터지지 않고 참조가 연결되지 않기에 비즈니스 로직 시에 문제가 발견될 수 있다
Bean을 찾지 못하였거나, 여러 Bean이 존재하고, Primary, Qualifier, Order 등이 설정되지 않은 경우
생성자 주입은 필드, 메서드 방식보다 적게 Reflection을 사용한다.
bean을 생성할 때, 다른 bean 정보를 가져오는 Dependency Lookup까지만 사용한다.
생성자 주입이 Reflection을 사용하지 않는다는 것은 Bean 생성 시에 주입하기에 다른 방식과 달리 추가적인 비용이 없음을 의미한다.
이는 Bean이 Container에 등록된 시점에서 다른 Bean을 생성하고 주입하기 때문이다.
생성자 주입은 자바 코드 그대로 해당 객체를 생성하는데 필수적인 요소를 지정하는 것이다. 그렇기에 A가 필수적인 인자로 B를 가지고 동일하게 B가 A를 가진다면 당연히 Bean 생성 그 자체가 되지 않는 것이다.
스프링은 이를 확인하고 순환 참조임을 알려준다.
생성자 이외의 유형의 주입을 사용하는 경우 종속성이 필요할 때 주입되기에. 생성자 주입이 진행되는 컨텍스트로드 시점이 아니므로 순환 참조가 발견되지 않는다. 결국 Autowired는 우선 Dependency LookUp을 application context에 존재하는 빈 정보를 가져오고 Injection 하는 것이기에 선택적인 인자 주입이 가능하다.
사용자의 권한을 인증, 인가하기 위하여 ID와 비밀번호의 직접적인 전달을 하는 것은 탈취될 가능성이 존재한다. 이를 방지하고 안전하게 필요한 정보를 넘겨받기 위해 사용하게 된다.
Oauth는 권한 위임을 위해 사용되는 프로토콜이며, 직접적으로 인가를 수행하기보단 요청할 수 있는 흐름과 수단을 제공한다.
웹 또는 모바일에서 개인 정보에 대한 액세스 권한을 요청받은 경우 해당 프로토콜을 사용하였을 것이다.
Oauth의 장점
HTTP / HTTPS를 이용해 동작하는 프로토콜이기에 범용적으로 사용할 수 있다.
단순하게 구현할 수 있고, 많은 양의 레퍼런스를 가진다.
스펙이 강제하는 부분이 적고, 선택적으로 제공하거나 자유함으로 필요에 맞게 변경할 수 있는 부분이 많다.
Oauth의 단점
Oauth 스펙은 대부분의 요소 포맷 등이 명확히 정의되어있지 않고 확장 가능하기 때문에, 설계의 유연함을 가지지만 반대로 여러 Oauth 서비스가 크게 다른 구현부, 확장 점을 가짐으로써 각 서비스가 호환이 되지 않을 수 있다.
스펙상 토큰의 수명 주기와 범위가 지정되어 있지 않기에 토큰이 만료되지 않을 수도 있다. 구현을 통해 해결할 수 있다.
서비스에 필요한 사용자 정보를 가져오기 위해 추가적인 요청이 있을 수 있다. JWT로 대체 가능하다.
스펙상 SSL과 TLS를 권장하나, 강제하지 않고 설정하지 않는한 일반적인 HTTP 프로토콜을 사용하기에 통신 도청과 토큰 탈취 등이 쉽게 일어날 수 있다.
Oauth의 취약점
CSRF
Access Token 생성시 state 매개변수를 이용함으로써 방지할 수 있다.
해당 변수 검증 누락이나 미흡시 토큰을 탈취당할 수 있다.
Convert Redirect
redirect_uri 파라미터에 대한 검증 누락이나 미흡시 발생하는 취약점이다.
변조된 URI를 통해 공격받을 수 있다.
Full Path 검증 등 강력한 방법을 사용하여야 한다.
Oauth의 구성 주체
Resource Owner (User, Device)
Resource Server에서 제공하는 기능을 이용하려고 하는 주체이며, 클라이언트에게 API 접근 권한을 위임할 수 있다. 이는 인증 정보 전달과 권한 위임 절차 수행을 통해 이루어진다.
별도의 소프트웨어 구성 요소가 아니라 서비스(리소스 서버의) 사용자를 의미한다.
ID와 password등을 이용해 Resource Client에 접근 권한을 인가한다. (액세스 토큰)
Resource Client (Somaeja)
리소스의 소유자를 대신하여 인가 서버를 통해 발급된 액세스 토큰을 가지고 Resource Server의 API와 상호작용을 하는 주체이다.
Resource Owner로부터 권한 인가를 받아 액세스 토큰을 획득하고 API 서버에게 요청한다.
Resource Server (Google, Facebook)
보호된 Resource를 관리하며 Client에게 Resource와 관련된 API를 제공한다.
전달받은 액세스 토큰이 유효한지 확인하기 위해 매번 Authorization Server와 통신한다.
이러한 방식은 비효율적인 구조를 가지기에 인가 내역을 캐싱하거나 JWT등을 이용한다.
Authorization Server (Facebook-auth Server, Google-auth Server)
인가 서버는 리소스 소유자와 클라이언트의 정보를 인증하고, 권한을 위임할 수 있는 메커니즘을 제공한다.
Resource Server가 올바른 액세스 토큰을 받았는지 검증 요청하는 것을 처리하고, 만료된 액세스 토큰을 폐기하기도 한다. 추가적으로 Resource Client의 요청을 제어하기 위해 해당 권한들을 제어할 수 있는 Client ID와 Secret 정보를 관리한다.
Oauth의 구성 요소
Access tokens
클라이언트에게 권한이 위임되었다는 것을 나타내기 위해 인가 서버가 클라이언트에게 발급해주는 요청 정보이다.
사용자가 제공한 인증 정보를 통해 만들어진 인가 코드와, 클라이언트의 자격 증명 정보, 검증 정보, 토큰의 접근 권한 등급 등의 정보들을 통해 만들어진다.
제네릭이 도입되기 이전에는 상황에 따라 각기 다른 데이터를 다루기 위해 최상위 클래스인 Object를 사용하여 코드를 작성하곤 하였다.
그를 통해서 어떠한 데이터라도 받아 저장할 수 있었으나 몇가지 문제가 있었는데.
저장했던 값을 사용해야 할 때 명시적으로 캐스팅을 해서 사용해야 한다.
잘못된 캐스팅을 통한 오류가 발생할 수 있다. (String → Integer 등..)
들어온 타입에 대한 검증하는 로직이 추가로 들어가야한다. ( instanceof 결국 런타임에서 발견 )
에러의 유무를 컴파일 단계에서 체크할 수 없다. (사전에 방지할 수 없다.)
즉 모호하고 찾기 어려운 잠재적인 오류를 가지게 된다. Reifiable type의 문제점!
이러한 문제를 방지하기 위해서는 컴파일 시에 타입 체크를 해서 사전에 실수를 방지하고, 명시적인 캐스팅에 대한 런타임 에러가 해결되어야 했다.
그를 위해 제네릭이 등장하게 되었다.
제네릭의 기능?
Generic은 해당 기능들을 제공한다.
컴파일러를 통해 타입 체크가 가능하다. 즉 컴파일 과정에서 문제를 제거할 수 있다.
컴파일 시점에서 사용되는 타입을 체크하여 해당 타입에 맞게 컴파일한다.
일반적으로는 캐스팅 비용과 타입 체크의 비용이 들어가지 않는다.
타입의 경계를 지정하여 제한할 수 있다.
Generic을 사용하게 발생하는 제약으론
Primitive Type을 사용할 수 없다.
컴파일 이후에 타입이 소거되기 때문에 런타임에서 타입 체크를 할 수 없다. Non-Reifiable
해당 문제는 Java 간의 하위 호환성을 위해 VM에서 지우는 방식이다.
타입 소거로 인해 캐스팅 코드가 만들어질 수 있다.
등이 있다.
구체화 타입, 비 구체화 타입, Erasure?
Reifiable type?
객체의 Type 정보를 Runtime 시에 확정하여 Application이 끝날 때까지 유지되는 타입이다.
제네릭 도입 전, 유사한 사용법을 위해 사용했던 Object 배열 방식은 런타임 시에 타입을 검증한다.
// 컴파일
Object[] array = new Long[10];
// 런타임
Long[] var2 = new Long[10];
var2[0] = Long.valueOf(1L);
non-Reifiable type? - Erasure
객체의 Type 정보를 컴파일 시점 시에 확인하여, 동일한 동작을 보장한 뒤 Runtime 전에 소거된다.
자바의 제네릭은 비 구체적인 타입을 가지며, 컴파일 시에 타입을 검증한다.
// 컴파일
List<String> list = new ArrayList<>();
// 런타임
ArrayList list = new ArrayList();
Type(class) Erasure
클래스의 파라미터 타입을 제외하고 첫 번째로 바인딩된 타입을 사용하거나 없는 경우 Object 타입으로 설정한다.
미 바인딩 시
public class Value<T> {
private T value;
public T getValue(){
return value;
}
public void setValue(T value){
this.value = value;
}
}
--------------------
// 컴파일 후
public class Value {
private Object value;
public Object getValue(){
return value;
}
public void setValue(Object value){
this.value = value;
}
}
바인딩 시
public class Value<T extends A> {
private T value;
public T getValue(){
return value;
}
public void setValue(T value){
this.value = value;
}
}
--------------------
// 컴파일 후
public class Value {
private A value;
public A getValue(){
return value;
}
public void setValue(A value){
this.value = value;
}
}
Method Type Erasure
메서드의 파라미터 타입은 바인딩되지 않은 경우 Object, 바인딩되었을 때에는 해당 클래스로 변환된다.
미 바인딩 시
public static <E> void someMethod(List<E> list) {
System.out.println(list.toString());
}
--------------------
// 컴파일 후
public static void someMethod(List<Object> list) {
System.out.println(list.toString());
}
바인딩 시
public static <E extends String> void someMethod(List<E> list) {
System.out.println(list.toString());
}
--------------------
// 컴파일 후
public static void someMethod(List<String> list) {
System.out.println(list.toString());
}
제네릭 사용법
제네릭의 변수 네이밍 관례
E : Element = Collection
K : Key
N : Number
T : Type Parameter
V : Value
S, U, V : 두 번째, 세 번째, 네 번째에 선언된 타입
제네릭 선언 및 사용 방법
public class Value<T> {
private T value;
public T getValue(){
return value;
}
public void setValue(T value){
this.value = value;
}
}
---------------------------------
public static void main() {
Value<String> stringValue = new Value<>();
stringValue.setValue("hello");
Value<Long> longValue = new Value<>();
longValue.setValue(10L);
}
제네릭 주요 개념 (바운디 드 타입, 와일드카드)
Type Unbound 란? - Unbounded WildCard
모든 Type을 허용하는 것을 나타낸다.
별도의 상태를 저장하지 않는, 파라미터에 의존하지 않는 메서드를 만들 때 사용한다.
// List<Object> list
List<?> list
Type bound 란?
특정 타입의 객체를 다른 타입의 객체로 변환할 수 있는 범위를 말한다. - 가변성
공변성 : 받은 Type을 확장(extend)하는 하위 Type 들에 대해서도 허용한다. 서브타입 와일드카드
여러 하위 클래스들이 하나의 상위 클래스 (분류)에 속한다.
메서드의 반환 타입에 사용할 수 있으나, 파라미터로는 사용할 수 없다.
리스코프 치환 원칙 ( is a kind of )을 생각하면 좋다.
// <E extends String> 형식은 지원하는 데이터 유형의 범위를 선언하는 데 사용된다.
List<E extends Object>
List<E extends Collection>
// 파라미터로 주어진 데이터 유형을 특정 범위로 제한하는 데 사용된다.
List<? extends Object>
Multiple type 제약 지정
해당 경계 안에는 제약 조건을 모두 만족하는 값만을 넣을 수 있다.
// 해당 인터페이스들을 구현한 객체만을 받을 수 있다.
List<E extends Serializeble & AutoCloseable & Cloneable>
무공변성 : 받은 Type 만을 허용한다.
List<String> list = new ArrayList<>();
String str = "string";
public int add(int a, int b)
반공변성 : 받은 Type이 구현하는 상위 타입만을 허용한다. 슈퍼 타입 와일드카드
메서드의 파라미터에 사용할 수 있으나, 반환 타입으로는 사용할 수 없다.
List<? super SomeClass> // 반환하는 값에 대해서 사용이 가능하다.
// 값을 쓰는 것이 가능하다.
제네릭 메서드 만들기
메서드의 선언부에 제네릭 타입을 사용한 메서드를 의미한다.
제네릭 타입의 선언 위치는 반환 타입 앞에 <>를 감싼 상태로 선언한다.
와일드카드의 경우 선언 순서를 변경할 수 있다.
public static <E> T someMethod(List<E> list) {
return list.get(0);
}
public static <T> void print(T val) {
System.out.println(val.toString());
}
public static <T, S> void print(T strVal, S longVal) {
System.out.println(strVal.toString() + longVal.toString());
}
// 참고용?
public static void someExample(List<? extends String> val) {
System.out.println(val.get(INDEX).toString());
}
public static <E extends String> void someExample2(List<E> val) {
System.out.println(val.get(INDEX).toString());
}
---------------------------
private static int INDEX = 1;
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
list1.add("string");
print(someMethod(list1.get(INDEX)));
List<Long> list2 = new ArrayList<>();
list2.add(1L);
print(someMethod(list2.get(INDEX)));
print(list1.get(INDEX), list2.get(INDEX));
}
JPA를 동작시키기 위한 내부 객체들이 이 Factory를 통해 생성되고, 구현체에 따라선 커낵션 풀도 별도로 구성하므로 생성 비용이 크다고 한다.
여러 EntityManagerFactory를 생성하여 공유할 수 있으나. 하나의 객체만 생성해서 전 영역에 공유하는 것이 권장된다. (configuration lite mode를 사용하면 여러 개를 생성하고 공유할 수 있을 것 같다.)
EntityManager (Persistence Context)
엔티티 정보를 저장하고 있는 환경. 요청 결과가 DB에 반영되기 이전에 위치하는 논리적인 영역을 의미한다. 가상의 데이터베이스 영역이라고 봐도 무방하다.
사용자의 요청을 받아 Connection을 통해 DB와 로컬 트랜잭션을 맺으며, 데이터를 관리하고 영속적으로 반영하는 혹은 삭제하는 책임을 가지는 객체이다.
JPA의 대부분의 기능은 해당 객체가 지원한다. 커넥션과 밀접한 관계를 가지기에 스레드 간의 공유, 재사용이 어렵다.
멀티스레드에 안전하지 않은 상태가 된다는 것 아닐까?
Entity의 Life Cycle
비영속 (new, transient) :영속성 컨텍스트와 관계가 없는 새로 생성된 상태
// 비영속 상태의 Entity. 즉 entityManager가 모르고 있는 Entity이다.
Entity entity = new Entity();
entity.setXxxA("test");
entity.setXxxB("value");
영속 (managed) : 영속성 컨텍스트에게 관리되는 상태 (환경 안에 정보가 있는 상태)
JPA는 기본적으로 트랜잭션 안에서 데이터를 변경하여야 한다.
// 요청에 따른 entityManager(Persistence Context) 생성
EntityManager entityManager = entityManagerFactory.createEntityManager();
// 영속성 컨택스트에 대한 글로벌 트랜잭션 관리 시작
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
// 비영속 상태의 Entity.
Entity entity = new Entity();
entity.setXxxA("test");
entity.setXxxB("value");
// 영속성 컨택스트에 Entity 정보를 저장. 즉 entityManager가 알고있는 Entity이다.
entityManager.persist(entity);
// transcation 결과 반영 - (DB 저장 X, 영속성 컨텍스트에만 반영)
transaction.commit();
} catch (RuntimeException exception) {
// 위 로직 실패 시 결과 반영 X -> 원복
transaction.rollback();
} finally {
// 성공, 실패 상관없이 transaction 종료
transaction.close();
}
준영속 (detached) : 영속성 컨텍스트에 관리되다가 이후 분리된 상태
영속성 컨텍스트가 지원하는 기능을 사용할 수 없기에 비영속 상태와 유사하다. 다른 점은 식별자 값(id)이 존재한다는 점이다.
// 특정 회원 엔티티를 영속성 컨텍스트에서 분리한다.
// 쉽게 말하면 해당 엔티티를 영속성 컨텍스트에 존재하는 1차 캐시에서 삭제한다.
entityManager.detach(entity);
// 해당 영속성 컨텍스트에서 관리되는 모든 엔티티를 준영속화한다.
// 쉽게 말하면 영속성 컨텍스트의 1차 캐시를 초기화한다.
emtityManager.clear();
// 영속성 컨택스트를 종료시킨다.
entityManager.close();
삭제 (removed) : 엔티티가 삭제된 상태
// 해당 객체를 삭제. 즉 DB에 저장된 정보를 삭제하는 요청을 전송한다.
entityManager.remove(entity);
준영속과 삭제의 다른 점은 Entity가 DB에 반영되기 전이냐 이후이냐의 차이이다.
JPA Internal Flow
Persistence Context의 특징
영속성 컨텍스트와 식별자 값
영속성 컨텍스트는 @ID로 매핑된 값을 통해 엔티티를 구분한다. 즉 해당 값이 무조건 존재해야 하며, 없는 경우 예외가 발생한다.
데이터베이스 저장 시점
영속성 컨텍스트에 저장, 관리되는 엔티티는 flush()가 호출된 시점에 실제 DB에 반영된다.
엔티티 관리 방식의 장점 -아래
JPA 엔티티 관리 방식의 장점
1차 캐시
영속성 컨텍스트에서 관리되는 데이터를 우선적으로 조회하는 것을 의미한다.
사용자의 요청이 종료된다면 해당 영역도 제거되기에 하나의 비즈니스 로직에서만 이점을 제공한다.
전역적으로 관리되는 캐시는 2차 캐시라고 부른다.
해당 정보가 컨텍스트 안에 존재하지 않는다면,
DB에서 해당 정보를 조회한 후 (SELECT)
쿼리 결과를 이용하여 엔티티를 생성한 다음
컨텍스트 내부에 엔티티를 저장하고 반환한다.
1차 캐시는 내부적으로 Map을 통해 관리되며, @Id로 매핑된 값을 Key로 가진다. 이 값은 DB의 PK 값인 경우도 존재한다.*
동일성 보장
1차 캐시를 이용하여 반복 가능한 읽기 등급의 격리 수준을 Application 수준에서 제공한다. 즉 같은 트랜잭션 내에 여러 번 조회 쿼리를 진행하더라도 동일한 결과를 반환받는 것을 이야기한다.
이때에는 동일성 비교가 가능하다. → Map에서 동일한 엔티티를 제공한다.
트랜잭션을 지원하는 쓰기 지연
한 트랜잭션 내에서 여러 엔티티를 등록할 때 각각의 엔티티에 대한 Insert Query를 DB에 바로 요청하지 않고, 일종의 버퍼처럼 컨택스트 내부에 존재하는 SQL 저장소에 Query를 쌓아놓다가 1번에 반영할 수 있다.
entityManager.commit();
큰 오버헤드를 가지는 i/o 처리를 최소화할 수 있다.
변경 감지
영속성 컨텍스트에게 관리되고 있는 엔티티 정보에 대해서 Service logic에서 setter 등을 통해 수정을 가할 경우, 그 변경 결과가 트랜잭션을 커밋하는 시점에 영속성 컨텍스트에게 감지되고 최종적으로 Update Query가 DB로 전송되어 최신 정보로 변경하게 된다.
엔티티 정보와 엔티티 스냅샷 정보를 비교하여 감지한다.
스냅샷 정보는 DB에서 값을 읽어와 캐시에 저장한 시점의 엔티티 정보를 복사하여 저장해둔 것이다. 이것을 트랜잭션이 커밋된 시점에서 엔티티 정보와 비교하여 변경된 사항들에 대해서 Update Query를 생성하는 것이다.
엔티티 삭제도 동일 메커니즘을 공유하고 트랜잭션 커밋 시 Delete Query를 전송한다.
Flush?
영속성 컨텍스트의 변경 내용들을 데이터베이스에 반영시키는 기능이다.
영속성 컨텍스트에 저장된 정보를 비우는 것은 아니다.
Flush 발생 시 Flow는
엔티티에 대한 변경을 감지하고
추가, 수정, 삭제된 엔티티 정보에 대한 Query를 내부 SQL 저장소에 등록한 뒤
해당 정보를 DB에 전송한다.
이렇게 이루어진다.
Flush를 발생시키는 방법으로는
em.flush()를 통한 직접 호출
트랜잭션 commit() 을 통한 자동 호출
JPQL으로 만들어진 Query 실행 시 자동 호출
영속성 컨텍스트에만 관리되고 있는 영속 상태의 엔티티들은 사실상 DB에 존재하지 않으므로 JPQL을 통한 Query 요청 시 결과에서 이 것들을 확인할 수 없게 된다. 이를 방지하기 위해 미리 Flush()를 호출하여 DB에 반영한 뒤 정상적인 결과를 제공하게 된다.
GRASP : General Responsibility Assignment Software Patterns?
해당 내용은 상호작용하는 클래스 혹은 객체에 책임을 할당하는데 도움이 되는 개념과 방법 즉 패턴들로 이루어져 있습니다. 이러한 내용들을 준수함으로써 좋은 객체지향 디자인 패턴을 만들어낼 수 있습니다.
Responsibility?
책임은 SOLID나 지금 다룰 GRASP 등 Oriented-Object-Design 패턴과 원칙에서 핵심이 되는 개념입니다. 이는 클래스와 객체가 어떤 메시지(요청)에 대해 처리할 수 있거나, 적절한 행동을 해야 하는 의무가 있는 경우 해당 객체가 이러한 책임을 가진다라고 이야기할 수 있습니다.
책임은 메서드를 통해 구현되게 됩니다.
Example.
String은 문자열을 표현하고 처리하는 책임을 가집니다.
File은 파일에 대한 정보를 알려주고 처리하는 책임을 가집니다.
DTO는 데이터를 다른 Layer로 운반하고 특별한 경우 비즈니스 로직에 따라 데이터 값을 반영시키는 책임을 가질 수 있습니다.
이런 것이 모두 책임 입니다.
책임의 분류는 행위 관점과 상태 관점으로 나눌 수 있습니다.
행위 관점.
객체를 생성하는 행위, 결과를 계산하는 행위 - 객체 스스로 하는 것.
다른 객체의 행동을 지시하는 것 - Controller
다른 객체의 활동을 제어하고 조절하는 것 - Chain of Responsibility, Proxy
상태 관점.
private로 접근 제어된 데이터에 관하여 아는 것
관련된 객체에 대해 아는 것
자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
Creator
새로운 객체를 생성하는 책임을 말합니다.
이러한 책임을 가지기 위해서는 하나 이상의 조건을 만족하여야 합니다.
생성하는 객체를 포함합니다.
생성하는 객체에 대한 초기화 정보를 가집니다.
생성하는 객체를 사용합니다.
생성하는 객체에 대한 정보를 기록합니다.
이와 관련된 것은 Creation Pattern 중 Abstract Factory, Factory Method가 있습니다.
Information Expert
새로운 기능이나 방법을 추가하는 것은 그에 필요한 정보를 가지는 클래스, 객체를 대상으로 하여야 합니다. 그러한 클래스, 객체는 최소한의 변경으로 기능을 구현할 수 있기 때문입니다.
결국 객체가 가지는 데이터에 대한 기능을 직접 처리하게끔 하라는 것입니다.
하지만 모든 필드에 대한 무분별한 Getter, Setter는 캡슐화를 지키지 못합니다.
해당 행위를 주관하는 객체(Client, Service)에서 데이터 흐름을 만들어내는 객체(DTO)의 데이터를 꺼내오는 것보다는 다음 행위를 하는 객체에게 해당 객체를 전달하는 것이 캡슐화를 지키는 방법입니다.
그리고 상태에 대한 질의 후 분기해야 하는 로직의 경우 Tell Don't Ask 원칙을 준수하는 것이 좋습니다.
요청에 대한 비즈니스 로직과 요청을 전달, 지시하는 로직은 분리되어야 합니다. 그리고 우리는 일반적으로 MVC Pattern을 통해 이러한 요구사항을 만족합니다.
Controller는 요청을 받고 적절한 행위를 하는 객체에게 지시하는 행위 관점의 패턴입니다.
이러한 책임을 가지는 객체는 내부적으로 별도의 비즈니스 로직을 가져서는 안 되며, 요청을 전달, 위임하는 것에 중점을 두어야 합니다.
Low Coupling
낮은 결합도를 준수하는 것은 시스템 설계의 전체적인 결합도를 낮게 유지되도록 구현하고 책임을 할당하라는 의미입니다. 객체 간의 결합이 없을 수는 없기에 우리는 최대한 영향을 주지 않도록 노력하여야 합니다.
이를 위해서는
클래스 간의 종속성을 낮추어야 합니다.
한 클래스의 변경이 다른 클래스에 주는 영향이 적어야 합니다.
객체는 더 높은 재사용 가능성을 가져야 합니다.
이는 Interface 등을 이용한 상호 작용이나 Facade 패턴 등을 사용하여 이루어낼 수 있습니다.
반대로 높은 결합도는 각각의 객체가 서로의 구현 세부사항을 인식할 때(접근 가능할 때, 사용할 때) 발생되게 되는 것입니다. 이런 경우 한 객체의 변경이 다른 객체의 변경을 야기하게 됩니다. extend의 경우 이런 변경에 대한 파급효과가 강해지게 됩니다.
High Cohesion
높은 응집력을 준수하라는 것은 객체 책임에 의해 변경될 수 있는 요소들을 한 곳에 모아서 관리하라는 것입니다.
이것을 통해 요구사항으로 인해 책임의 변경이 이루어진다면, 그러한 영향이 하나의 객체에 대해서만 이루어질 수 있습니다.
이것과 관련된 원칙은 SOLID의 Single responsibility principle가 있습니다. 이 원칙은 객체를 변경할 수 있는 책임이 하나만 존재해야 한다는 원칙입니다.
Indirection
간접이란 두 객체 간의 직접적인 결합을 분리하고 간접적인 결합을 지원하게끔 설계하라는 것입니다. 이는 별도의 중개자 객체를 통해 Dependency Injection을 진행하거나, 요청에 대한 위임, 지시하는 컨트롤러와 같은 요소를 도입하는 것을 말합니다.
Observer, Facade, Bridge, Mediator, Adapter는 간접 접근 방식을 활용하는 패턴입니다.
Polymorphism
하나의 타입을 가지는 서로 다른 객체들이 동일한 책임, 메시지에 대해서 서로 다르게 반응하는 것을 말합니다. 즉 책임을 공유하는 상태입니다.
이러한 원칙은 객체들 사이의 대체 가능성을 의미하며 그를 통해 설계를 유연하고 재사용 가능하게 만들 수 있습니다.
각각의 요청에 대하여 Reflection API 등의 동적 로딩을 통하여 Runtime Dependency를 구현하거나 Chain of Responsibility와 같은 행위 패턴을 구현하고 분기를 위한 상태 변수를 전달함으로써 컴파일 시점에서 필요한 객체를 Dependency Injection 할 수 있습니다.
Protected Variations
요구사항에 대한 책임, 동작 변화는 객체의 내부에서만 이루어져야 하며, 공용 인터페이스를 동일하게 유지함으로써 Client 역할을 하는 객체에게는 변경 사실을 숨겨야 합니다.
변경될 여지가 있는 로직에 대해서는 객체 간의 캡슐화를 지켜야 한다는 것입니다.
이와 관련된 원칙으로는 SOLID의 Open/closed principle이 있습니다. 이 원칙은 객체의 책임의 확장과 변경이 다른 객체의 행위에 영향을 주지 않아야 된다는 것을 말합니다.
Pure Fabrication
객체가 광범위한 책임을 가지지 않도록 (God Class) 분리하라는 것을 말합니다. 이는 기능에 대한 공통적인, 비즈니스 로직을 하나의 객체를 만들어 담당하게끔 하는 것을 뜻합니다.
Layer Architecture에서 이러한 책임을 담당하는 객체를 Service라고 합니다.