애노테이션은 애물단지다. 사용하기 편리해보이는 듯 하지만 이를 이용하도록 라이브러리나 프레임워크를 만든다면, 일반적인 자바의 타입 구조를 활용해서 만드는 것보다 몇배의 수고가 들어간다. 애노테이션은 타입 정보가 아니기 때문에 정보를 읽고 활용하려면 리플렉션 API를 사용해야 한다. 게다가 상속이나 구현을 통해서 정보가 전이되지도 않는다. 메타 애노테이션도 마찬가지다.

애노테이션을 이용해서 코드를 작성하도록 뭔가 만들어보려면 그래서 이런 애노테이션의 제약에 일정한 관례를 부여해야 한다.

스프링을 보자면,

컴포넌트 스캔에서는  클래스의 메타 애노테이션을 모두 뒤져서 @Component가 어느 단계라도 존재하면 빈 후보로 선택한다. 그래서 @Configuration만 달아도 스캔된다. @Configuration 애노테이션의 정의를 보면

@Component
public @interface Configuration {

처럼 @Component가 메타 애노테이션으로 달렸으니까.

그런데 메타 애노테이션에 뭔가 존재한다고 이게 자동으로 애노테이션에 상속되는 것이 아니니, 메타 애노테이션에 뭔가 있는지 알려면 리플렉션을 이용해서 계속 뒤져야 한다.  클래스와 인터페이스로 계층구조를 만들어면 타입정보가 상속되기 때문에 손쉽게 타입 체크를 하거나 다형성을 활용할 수 있는 것과 대조된다. 게다가 정해진 규칙도 없으니 만든 사람이 정한 규칙을 모르면 헷갈린다.

그래서 Josh Bloch은 인터페이스로 타입 정보를 넣을 수 있는데다 애노테이션을 사용하지 말라고 이펙 자바 2에서 경고하기도 했다.

또 다른 문제는 수퍼 메소드나 수퍼 클래스, 인터페이스 등에 붙은 애노테이션 정보가 자신에게도 적용되냐 이거다. 당연히 애노테이션에는 상속 개념이 없으니 될지 안될지는 그걸 활용하는 프레임워크나 라이브러리 개발자가 어떤 식으로 애토네이션 정보를 찾아가느냐에 달렸다. 수퍼에 있으면 상속을 하는지, 서브에도 달렸으면 오버라드를 하는지, 결합해서 새로운 조합을 만드는지, 에러를 내는지 등등은 순전히 개발자 맘이다.

@Transactional이나 @RequestMapping등을 생각해보면 될 것이다. @Transactional에 대체(fallback) 룰이 적용되는 것이지 @RM에서 상속시 서브 메소드/타입의 애노테이션 정보가 수퍼 것을 오버라이드하는지 어쩌고의 여부는 토스3 같은 쓸데 없어 보이는 것까지 다 다루는 하드코어 스프링 책을 보지 않으면 알기 쉽지 않을 것이다.

애노테이션에는 일관성도 없고 사용방식도 멋대로기 때문에 상당히 불편하다. API Doc을 종일 들여다 보면서 개발해야 실수 하지 않고 겨우 개발이 가능한 위험한 라이브러리를 다루는 느낌이다.

그래도 타입 정보를 이용하기 보다는 관례와 대충대충을 사랑하는 개발자들의 입맛에 잘 맞게 때문에 계속 사용될 듯 하다. 자바 코드는 무늬만 놔두고 애노테이션만 왕창 넣어서 사용하는 프로그래밍 방법이 나올지도 모르겠다. 요즘 작성하는 하이버네이트 엔티티 클래스에선 애노테이션이 나머지 코드보다 양이 많다. Lombok으로 G/S까지 없앴으니 필드 뿐인데 거기에 관계 정보, 매핑 정보, 스키마 정보, 검증 정보 등의 코드와 XML등을 대체해준다는 각종 메타 정보가 듬뿍 달라 붙는다.

애노테이션은 이제 DSL로 사용되는 것 같다.  새로운 애노테이션 종류가 나오면 새로운 언어를 익히는 마음으로 문법과 용법(usage) 등을 익혀야 할 것이다.

내가 애노테이션 정보를 직접 가져와 사용할 필요가 있을 때는 스프링에 있는 AnnotationUtils의 findAnnotation() 메소드를 애용한다. 이 메소드는 @RequestMapping을 비롯한 여러 애노테이션을 다룰 때 사용된다. 메소드/타입 정보와 애노테이션을 파라미터로 받아서 메소드/타입에 해당 애노테이션이 있는지 찾아 있으면 그 정보를 돌려준다. 없으면 수퍼 타입/메소드에 애노테이션이 있는지 Object까지 올라가면서 몽땅 뒤진다.

그래서 이 findAnnotation()을 사용해서 애노테이션 정보를 다루는 기능은 수퍼 클래스/메소드에 동일한 타입의 애노테이션이 있는 경우 마치 오버라이딩한 것처럼 보인다. 수퍼에만 있으면 그걸 사용하는데 서브에도 또 있으면 먼저 발견하니까 수퍼는 그냥 무시하기 때문에 그렇게 보일 뿐이지 상속되는 거나 오버라이드 되는 건 아니다.

대충 스프링에는 이런 오버라이딩 비스무리한 관계가 있지만 모든 애노테이션에 대해서 다 그렇다고 볼 수는 없고, 애노테이션 정보를 메소드와 타입 레벨의 것을 조합되서 사용되기도 하고, 조합했을 때 OR로 하기도 하고 AND로 하기도 하고, 모든 가능한 조합을 다 만들기도 하고, 한쪽을 배제하고 다른 쪽의 정보를 우선하기도 한다. 꼼꼼하게 애노테이션을 이용하는 프레임워크가 아니면 잘못 사용해도 에러를 내지도 않는다. 아무튼 애물단지다.

오늘의 결론은 "애노테이션을 사용하도록 프레임워크나 라이브리를 만드는 개발자들은 애노테이션 사용 방법에 대해서 문서 좀 제대로 만들란 말이야!"

맨날 소스 뒤져가며 어떻게 사용해야 할지 찾기 정말 피곤해.

제목을 원래 "WebApplicationInitializer을 이용한 컨텍스트 초기화"라고 쓰려고 했다가 바꿨다. 제목 낚시의 달인 영회에게 배운대로 제목에서 친근하지 않은 단어는 빼기로.

서블릿 3.0을 사용하면 기존에 web.xml을 이용해서 서블릿 컨텍스트를 초기화하던 작업을 여러 파일로 쪼갤 수도 있고, 자바 코드를 이용할 수도 있다. 요즘엔 스펙 문서 읽는 게 너무 지루해서 기피하다보니 나도 서블릿 3.0 스펙을 꼼꼼하게 읽어본 것은 아니지만, 대충 봐도 그런 내용이 나온다.

자바코드와 web.xml 파일을 같이 사용할 수도 있고, 아예 web.xml 없이 자바코드만으로 초기화 작업을 할 수도 있다. 어짜피 web.xml은 ServletContext 오브젝트 초기화하는 데 사용되던 메타정보니까 직접 ServletContext를 다루면 되는 것이겠지.

이때 사용되는게 javax.servlet.ServletContainerInitializer인데 좀 특이하다. META-INF/services/javax.servlet.ServletContainerInitializer 파일(단순 텍스트 파일이다) 안에 구현 클래스를 지정해야 하고, 이게 또 jar 파일내에 있어야 하는 것 같다. web.xml이라는 간단한 표준 설정 파일을 없앴기 때문에 웹 앱 내의 모든 클래스, 심지어 jar 내의 클래스도 다 뒤져야 하는 부담이 생긴다. 그거 말고도 스캐닝할게 많은데. 그래서 스캐닝의 부담을 줄이기 위해서 javax.servlet.ServletContainerInitializer(이게 파일 이름이다)를 쓴 꼼수가 참 눈물난다. web.xml 같은 XML 없이도 개발이 가능하다고 큰 소리치긴 해야 겠는데, 그래도 ServletContainerInitializer 위치는 정해진데 명시해줘야 할 것 같고, 그렇다고 initializer.xml을 만들라고 하자니 애개 XML 또 들어갔네 할 것 같고. 그래서 파일 이름을 javax.servlet.ServletContainerInitializer으로 결정. 그래 XML 없다. 인정.

아무튼 ServletContainerInitializer는 jar 안에 META-INF/.. 과 함께 있을 경우에 동작하는데, 얘를 구현한 클래스에 @HandlesTypes를 사용하면 @HandlesTypes 안에 지정한 타입의 클래스를 모조리 찾아서 onStartup()의 파라미터로 넣어준다.

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> classes, ServletContext ctx) throws ServletException;
}

이 방식은 jar로 패키징된 프레임워크가 애플리케이션내에 특정 타입으로 작성된 클래스를 사용해서 서블릿이나 필터, 리스너 형태의 프레임워크 모듈에서 사용하는 것을 가능하게 해준다. 예를 들어, 필터를 사용하는 보안 프레임워크를 만들었다고 하자. 여기에 사용자가 보안과 관련된 설정을 SecurityConfig이라는 인터페이스를 구현한 클래스를 만들어서 그 안에 configSecurity() 메소드 에서 구현하게 만들도록 한다. 그리고 보안 프레임워크 jar안에 ServletContainerInitializer를 구현한 초기화 클래스 SecurityInitializer를 만들고, META-INF에 등록하고, 그리고 @HandlesTypes(SecurityConfig.class)를 붙여주면, 서블릿 3.0 컨테이너는 SecurityConfig 타입의 클래스를 모조리 찾아서 SecurityInitializer의 onStartup()에 넘겨준다. 그러면 여기서 SecurityConfig 클래스로 작성된 보안관련 설정을 configSecurity() 메소드를 실행해가며 수집한 뒤에 이를 초기 설정으로 해서 보안관련 Filter를 셋업하고, 얘를 최종적으로 ServletContext에 필터로 등록해주면 된다.

보이는 것과 다르게 사용하기는 어렵지 않다.

XML 파일 같은 중앙집중형 설정방식이 사라졌을 때 어떻게 접근해야 할지에 대한 나쁘지 않은 아이디어 같다.

아무튼 스프링도 이걸 이용한다. spring-web.jar에 ServletContainerInitializer를 구현한 클래스가 있다. 당연히 META-INF/…도. @HandlesTypes은 WebApplicationInitializer다. 스프링의 ServletContainerInitializer 구현 클래스인 SpringServletContainerInitializer는 직접 서블릿 컨텍스트에 뭔가를 등록하지 않는다. 대신 그 작업을 WebApplicationInitializer로 다 위임한다. SpringServletContainerInitializer는 @Order 정보를 가지고 실행 순위 조종해주는 정도.

이제 WebApplicationInitializer를 만들어서 여기서 원하는 대로 앱 컨텍스트 만들고 리스너, 서블릿 등에 등록하고, 지지고 볶고 하면 된다. @HandlesTypes은 설정정보 수집 내지는 실제 등록될 서블릿이나 필터가 소모할 대상정도고, 서블릿 컨텍스트 초기화는 ServletContainerInitializer 구현 클래스내에서 일어나는 것이 원래 서블릿 3.0의 의도라고 생각되는데, 스프링은 이를 개발자가 작성하는 초기화 클래스에서 직접 컨텍스트 초기화(주로 스프링 컨텍스트 등록)에 사용하도록 확작한 셈이다. 스프링의 SpringServletContainerInitializer 문서를 보면, 그래서 주절주절 이에 대해서 설명이 많은데. 눈이 침침해서 다 읽기가 싫으네.

아무튼 어제 밤에 살펴본 SpringServletContainerInitializer와 WebApplicationInitializer에 대한 소감은 이정도로 마무리. 서블릿 3.0을 본격적으로 쓰기 시작하게 되면 그때 제대로 살펴봐야지.

web.xml 없이 WebApplicationInitializer에서 스프링 컨텍스트 초기화&등록 하는 것과 관련된 자세한 설명은…

스프링 3.1에 도입된 런타임 환경 추상화는 프로파일과 프로퍼티 소스로 구성되어있다. 스프링 설정 파일 외부에 설정용 프로퍼티 값을 두고 관리하는 방법을 사용하려면 프로퍼티 파일(.proerties, .xml)에 값을 넣고 XML에서 ${} 프로퍼티 치환자를 사용하거나 SpEL을 이용해서 <util:properties/>로 정의된 프로퍼티 빈의 값을 끌어오는 방법뿐이었다. 참. 스프링이 ApplicationContext나 BeanFactory처럼 기본으로 @Resource 해주는 systemProperties와 systemEnvironment 빈을 이용하는 방법도 있긴하다. 아참. 웹이라면 컨트롤러 등에서 서블릿 컨텍스트를 가져와서 애트리뷰트나 컨텍스트 파라미터를 읽어도 되겠지. 아차차. <jee:jndi-lookup>으로 JNDI에 설정해둔 값을 가져오는 방법도 가능하게다. 이런. 빈 초기화 하는 즈음에 DB에 넣어둔 프로퍼티 값을 읽거나 RestTemplate으로 다른 서버에서 끌어오는 방법도 가능하겠네.

애플리케이션의 구성 정보를 담은 XML설정파일 말고 주로 런타임 환경과 관련이 있는 속성은 이렇게 다양한 위치에 넣어두고 다양한 방법으로 가져와서 사용할 수 있다. 그런데 이 모든 방법이 대부분 “키와 값의 쌍으로 이루어진 애플리케이션 외부에 정의된 프로퍼티 값을 가져온다”는 공동점이 있으니 추상화를 하면 좋겠다.

그래서 스프링 3.1은 앞에서 말한 다양한 방법(환경변수, 시스템 프로퍼티, JNDI, 서블릿 컨텍스트 파라미터 , 서블릿 콘픽 파라미터, DB나 서버에서 끌어오기 같은 기타 등등)으로 정의된 프로퍼티를 ProperySource라는 개념으로 추상화하고, 이를 PropertyResolver에서 일관된 방법으로 가져올 수 있도록 만들었는데, 환경 추상화 API인 Environment가 바로 이 PropertyResolver의 서브 인터페이스다.

스프링은 고맙게도 프로퍼티 소스로 추상화 했다고 옛정을 버리지는 않았다. 원한다면 이전에 쓰던 placeholder configurer를 그대로 사용할 수 있다. <context:property-placeholder>와 ${}를 그대로 사용할 수 있다는 뜻이다. 대신 내부에선 프로퍼티 소스 추상화를 이용해 통합된 프로퍼티 정보에 접근하는 방법을 사용하도록 변경됐다.

자세한 내용은 올 봄에 나올지도(-_-;;) 모르는 토스3 개정판에서…

그런데 얼마전에 권남님의 http://kwon37xi.egloos.com/4665590 를 읽어보니

properties-placeholder는 부모 자식 관계에 있는 Application Context XML 설정파일들의 경우에도 동일한 properties 파일임에도 모든 Bean 설정 파일에 property-placeholder를 명시해줘야 하는 문제가 있다.

는 얘기가 나온다. 그래서 빈으로 프로퍼티를 정의하고 SpEL로 끌어오는 방법을 추천한다는.

부모-자식 관계의 컨텍스트 구조는 웹 환경에서 루트와 서블릿 컨텍스트가 분리되는 경우가 대표적인데, 루트에 <context:property-placeholder>를 넣어도 서블릿 컨텍스트의 ${}에는 안 먹는다는 얘기인 것 같다. 당연하다. 왜냐하면 기존 <c:pp>는 빈 팩토리의 ${}등이 들어있는 메타 정보(프로퍼티 값)를 가공하는 방식이다. pull이 아니라 push개념인 셈인데, 루트에 정의된 <c:pp>에서 아직 만들어지지도 않은 서블릿 컨텍스트의 메터 정보를 가공한다는 것은 불가능하기 때문이다. 반면에 SpEL은 다른 빈의 값을 끌어오는 pull 방식이라 컨텍스트 생성 시점에 이미 루트 컨텍스트를 부모 컨텍스트로 주입받은 서블릿 컨텍스트 입장에서 루트 컨텍스트의 빈으로 정의된 프로퍼티의 값을 가져오는 건 간단하다.

물론 하나의 프로퍼티 파일 정보를 루트와 서블릿 컨텍스트 간에 공유하게 하는게 바람직한지 시비를 걸 수도 있겠지만, 뭐 쓰는 사람 맘이지.

그리고, “모든 Bean 설정 파일에 p-p를”이라는 건 아닌듯 싶다. <import>를 쓰거나 configLocation에 *를 사용해서 한 컨텍스트가 여러개의 빈 설정 파일을 사용하게 하는 경우라면 p-p는 한번만 넣어줘도 된다.

아무튼 3.0은 그랬지만 3.1에선 더 이상 이런 문제가 없다. 3.1에선 런타임 환경 정보를 통해서 다양한 프로퍼티 소스를 하나로 통합해서 가져오게 하는 방식을 사용하기 때문에 개념적으로 pull인 셈이다. 심지어 <context:property-placeholder>를 사용하더라도 그렇다. 3.1 스타일로 @PropertySource를 사용해서 p-p와 분리한다면 말할 것도 없고.

그러니 3.1에선 프로퍼티 파일 하나를 일개 빈으로 설정하는 SpEL방식보다 프로퍼티 소스로 추상화해서 사용하는 방식을 추천하고, ${} 스타일을 원한다면 예전에 한번 설명했던 PropertySourcePlaceholderConfigurer를 사용하면 될 것이다. XML을 사용하지 않는 경우라면 @Value의 ${}보다는 Environment에서 직접 프로퍼티를 읽는 방식을 써도 좋다.

스프링이 기본적으로 제공하는 6가지 프로퍼티 소스(구현 클래스가 아니라 정말 소스)외에 커스톰 프로퍼티 소스를 사용하고 싶으면 ApplicationContextInitializer를 쓰면 되는데 이게 3.1.1에서도 살짝쿵 버그가 있는 것 같아서 조금 주의해야 할 것 같다. 이 얘긴 다음에.

스프링 3.1의 프로파일은 매력적이다. 이 좋은 걸 왜 진작에 안 만들었을까 싶다.

프로파일을 활성화 시키는 방법은 여러가지다. 컨텍스트의 Environment 오브젝트를 가져와 setActiveProfiles() 해주는 방법도 있고, 시스템 프로퍼티 설정, 환경 변수 설정, JVM 커맨드 라인 파라미터 설정 등등.

WAS에 올라가는 웹 앱이 여러개거나 시스템 관리자가 까칠해서 지원을 잘 안하주는 관계로 JVM 레벨의 프로파일 지정이 곤란하다면 web.xml에 넣는 방법도 있는데, 이 때 servlet context parameter와 servlet init. parameter 두가지가 다 가능하다.

그런데 주의할 점이 있다.

웹 환경에서 루트 앱 컨텍스트는 <listener>로 등록한 컨텍스트 로더가 담당하고,  앱 컨텍스트 옵션은 서블릿 컨텍스트 파라미터를 지정하는 <context-param>을 사용한다.

DispatcherServlet이 만드는 서블릿 앱 컨텍스트에 관한 설정은 <servlet> 안에 <init-param>을 사용해서 앱 컨텍스트 클래스나 파일 등을 지정할 수 있다.

이렇게 사용하던 습관을 가지고 <context-param>은 루트 앱 컨텍스트용이고, <init-param>은 서블릿 앱 컨텍스트용이라고 막연히 생각하고 있으면 active 프로파일 지정할 때 실수 할 수 있다.

왜냐하면 active 프로파일 설정에서는 <context-param>은 해당 웹 애플리케이션의 모든 앱 컨텍스트에 영향을 주기 때문이다.

<context-param>
    <param-name>spring.profiles.active</param-name>
    <param-value>dev</param-value>
</context-param>

이렇게 해 놓으면 루트 앱 컨텍스트 뿐 아니라 서블릿 앱 컨텍스트에도 active 프로파일이 dev로 잡힌다.

반면에 <servlet> 아래

<init-param>
    <param-name>spring.profiles.active</param-name>
    <param-value>dev</param-value>
</init-param>

 

이렇게 해 놓으면 이건 루트 앱 컨텍스트의 active 프로파일에는 영향을 주지 않는다.

어떻게 보면 이게 자연스럽게 보인다. 서블릿 컨텍스트(즉 웹 앱)가 범위가 더 크므로 서블릿 컨텍스트의 모든 컨텍스트에 영향을 미치는게 맞다. 반면에 서블릿 레벨의 파라미터는 해당 서블릿의 앱 컨텍스트에만 적용되야지.

문제는 앱 컨텍스트의 조건을 줄 때는 이게 분리된다는 점이다. 같은 파라미터 설정 방식이지만 용도에 따라 적용 범위가 달라진다. 그래서 착각할 수도 있으니 주의할 것.

다른 문제로는 루트 앱 컨텍스에만 active 프로파일을 지정할 방법이 없다. 있을지도 모르겠지만 아직까지 못 찾았다.

 

그런데 프로파일을 하나의 웹 앱 안에서도 다르게 줄 필요가 있을까? 없을 거라고 볼 수는 없겠지. 매우 규모가 큰 앱이라 계층별로 독립적으로 개발하고, DS서블릿도 여러개를 사용하는 경우라면 필요에 따라 각각 다른 active 프로파일을 줄 수 있을 것이다. 물론 프로파일을 전체적으로 통일해서 관리하는 게 바람직하겠지만.

오늘의 결론은…

앱 컨텍스트 설정 결과를 꼭 눈으로 확인해서 기대한 대로 원하는 빈이 구성됐는지 확인하자. 특히 프로파일 같은 거 지정은 착각하면 대박 사고가 날지도. 설정 결과 확인 방법은 여기 저기서 많이 얘기했으므로 안해도 되겠지.

스프링 오덕 말고는 관심없을 것 같은 얘기는 여기서 끝. (뭔말하는지 도통 모르신 분들에게는 죄송. 언젠가 이해되실 겁니다)

C-Thinker님의 Repository/DAO, DTO, 그리고 확장성과 퍼포먼스란 글과 거기서 인용된 [MyStory Series] Repository? DAO? 을 읽다가 DAO와 리포지토리를 그냥 뭉뚱그려서 같은 개념(주로 레이어를 가리키는 말)으로 사용하는 것이 눈에 띄였다.

‘리포지토리’는 Eric Evans의 DDD책이 나오고 DDD의 관심이 고조되면서 유행하기 시작해서 요즘엔 DAO를 대신하는 폼나는 용어로 많이 쓰이는 듯 하다. 예전에 많이 쓰던 UserDao 대신 UserRepository라고 사용하기도 하고, 스프링의 데이터 액세스 로직을 담은 빈에 사용하는 스테레오타입 애노테이션도 @Repository다.

그런데, DAO나 리포지토리가 그냥 비슷한, 서로 바꿔서 사용할 수 있는 용어일까.

한국 DDD의 아버지라고 불리는 이터너티님의 DAO와 REPOSITORY 논쟁이라는 날카로운 글을 보면 DAO와 리포지토리는 확연이 구분되는 다른 의미를 가진 용어이다. 물론 나는 이터너티님의 의견에 모두 동의하는 것은 아니지만, 어쨌든 원래 리포지토리라는 말이 정의되고 사용되기 시작한 기원을 따져보자면 이터너티님의 지적이 맞는 듯.

보통 DDD 책에서 리포지토리라는 용어를 처음 접한 경우가 많은데 사실 그보다 먼저 나온 마틴 파울러옹의 P of EAA(Patterns of Enterprise Application Architecture) 책에서 먼저 리포지토리가 소개됐다. P of EAA 내용을 소개한 웹 페이지(http://martinfowler.com/eaaCatalog/repository.html)에서도 그 내용을 볼 수 있다. 이 내용만 잘 살펴보더라도 단순히 데이터 액세스(매핑) 레이어를 리포지토리 레이어라고 부르는 것은 적절하지 않은 것 같은데.  뭐 IT 용어가 반드시 하나의 의미로만 사용될 이유야 없으니 부르고 싶은대로 부르겠다면 뭐 할말은 없지만.

웹 페이지보다 좀 더 자세하게 Repository 내용을 다룬 P of EEA 책의 Repository 항목을 보면 관련 샘플 코드도 나와있으니 관심 있으면 살펴보는 것도 좋을 듯. 재밌는 것은 P of EEA의 참고서적에 DDD가 나온다는 것. 출간은 안됐고 저술중인 것으로 나오는데, Repository 항목을 보면 이 책과 Evans에 관한 언급이 나온다. 웹 페이지에선 아예 책 추천도 하고.

그런데 transparent persistency를 지원하는 하이버와 같은 ORM에선 리포지토리의 구현이 상당히 직관적이기도 하다. 소위 리포지토리/DAO 레이어를 취급 안하는 본격 EJB3/JPA 문화에서는 그래서 다르게 받아들이기도 하는 듯. 전에도 개빈 킹은 하이버 사상으로는 DAO와 리포지토리에 대해서 끝까지 차이점을 모르겠다고 하기도.

레이어를 말할 땐 그냥 데이터 액세스 레이어라고 하는게 나에겐 가장 자연스럽지만, 그냥 DAO 레이어라도 해도 그만. 그래도 리포지토리 레이어라는 말은 글세.

그런데 전에도 비슷한 글을 썼던 것 같기도 한데… 흠. 뭐 어때.

© 2017 Toby's Epril Suffusion theme by Sayontan Sinha