Spring IoC와 DI에 대해 정리.
토비의 스프링 3.1과 스프링 철저입문 도서, 타 블로그의 정리 내용을 보고 정리했습니다.
책 위주로 정리한 내용이고 풀어서 정리되어있기 때문에 간단한 정리는 타 블로그 참고를 추천드립니다.
Reference
- 토비의 스프링 3.1
- 스프링 철저 입문
IoC와 DI에 대한 정리는 아래와 같은 순서로 진행.
- IoC Container란
- IoC Container의 종류와 사용방법
- Web Application의 IoC Container 구성
- Bean 설정 방식, DI 방식
- Autowiring, ComponentScan
- Bean Scope
- Bean 설정 분할과 profile별 설정
- Bean 생명주기, IoC Container 종료
정리 순서.
1. WebApplicationContext 계층구조
2. Web Application의 context 구성방법
3. RootApplicationcontext, ServletApplicationContext 등록
이번 포스팅은 웹 애플리케이션 안에 WebApplicationContext 타입의 IoC 컨테이너를 두는 방법을 정리한다.
자바 서버에는 하나 이상의 웹 모듈을 배치해서 사용할 수 있다.
스프링을 사용하면 보통 독립적으로 배치 가능한 웹 모듈(WAR) 형태로 애플리케이션을 배포하고 하나의 웹 애플리케이션은 여러개의 servlet을 가질 수 있다.
자바 서버 기술이 막 나온 시기에는 URL 하나당 하나의 servlet을 만들어 등록하고 각각 독립적인 기능을 담당하게 했지만
최근에는 많은 웹 요청을 한번에 받을 수 있는 대표 servlet을 등록해두고, 공통적인 선행 작업을 수행하게 한 후에,
각 요청의 기능을 담당하는 Handler라는 클래스를 호출하는 방식으로 개발하는 경우가 일반적이다.
이와같이 몇개의 servlet이 중앙 집중식으로 모든 요청을 다 받아 처리하는 방식을 Front Controller Pattern이라고 한다.
스프링도 프론트 컨트롤러 패턴을 사용하기 때문에 스프링 웹 애플리케이션에 사용되는 servlet의 숫자는 하나이거나 많아야 두세개 정도이다.
웹 애플리케이션 안에서 동작하는 IoC 컨테이너는 두가지 방법으로 만들어진다.
하나는 스프링 애플리케이션의 요청을 처리하는 servlet 안에서 만들어지는 것이고,
다른 하나는 웹 애플리케이션 레벨이서 만들어지는 것이다.
일반적으로는 이 두가지 방식을 모두 사용해 컨테이너를 만들기 때문에 두개의 컨테이너 즉, WebApplicationContext 오브젝트가 만들어진다.
스프링 애플리케이션의 진입 창구 역할을 하는 프론트 컨트롤러 servlet이 한개 이상 등록된다면, 그만큼 전체 컨테이너 개수는 더 늘어난다.
1. WebApplicationContext 계층구조
웹 애플리케이션 레벨에 등록되는 컨테이너는 보통 RootWebApplicationContext라고 불린다.
이 컨텍스트는 servlet 레벨에 등록되는 컨테이너들의 부모 컨테이너가 되고 일반적으로 전체 계층구조 내에서 가장 최 상단에 위치한 Root Context가 되기 때문이다.
웹 애플리케이션에는 스프링 애플리케이션의 프론트 컨트롤러 역할을 하는 servlet이 한개 이상 등록될 수 있는데 이 servlet에는 각각 독립적으로 ApplicationContext가 만들어진다.
이런 경우 각 서블릿이 공유하게 되는 공통적인 Bean들이 있고, 이런 Bean들을 웹 애플리케이션 레벨의 컨텍스트에 등록하면 된다.
그러면 공통되는 Bean들이 servlet 별로 중복돼서 생성되는것을 방지할 수 있다.
위 그림에서 Servlet A 와 Servlet B는 각각 자신의 전용 ApplicationContext를 갖고 있다.
동시에 두 컨텍스트가 공유해서 사용하는 Bean을 담아놓을 수 있는 컨텍스트가 존재하는데 이 컨텍스트는 각 서블릿에 존재하는 컨텍스트의 부모 컨텍스트로 만들어줘서 최상단에 위치하는 컨텍스트인 Root Context가 된다.
이렇게 구성하게 되면 Servlet A와 Servlet B의 컨텍스트는 서로 독립적인 Bean을 생성해서 동작하고 공통적인 Bean은 부모 컨텍스트인 RootApplicationContext가 만든 것을 공유해서 사용할 수 있다.
스프링에서 ApplicationContext 계층구조가 사용되는 가장 대표적인 경우다.
하나의 servlet이 웹 애플리케이션에 들어오는 모든 애플리케이션 요청을 처리할 수 있는 프론트 컨트롤러 역할을 하는데 굳이 두개 이상으로 나눠서 servlet을 구성하고 요청을 분산해야 하는가에 대해 의문이 생길 수 있다.
이렇게 두개의 servlet을 두고 사용하는 경우가 많지는 않은데 특별한 이유로 기존에 만들어진 servlet 레벨의 ApplicationContext와 그 설정을 그대로 유지하면서 새로운 기능이나 별도의 웹 기술을 추가하고 싶은 경우에 사용된다.
일반적으로는 스프링의 ApplicationContext를 가지면서 프론트 컨트롤러 역할을 하는 servlet은 하나만 만들어서 사용한다.
그럼 이렇게 프론트 컨트롤러의 역할을 하는 servlet을 하나만 만들어서 사용할건데 계층구조를 만드는 이유는??
전체 애플리케이션에서 웹 기술에 의존적인 부분과 그렇지 않은 부분을 구분하기 위해서다.
스프링을 사용한다고 해서 스프링이 제공하는 웹 기술만을 사용해야 하는 것은 아니다.
데이터 액세스 계층이나 서비스 계층은 스프링 기술을 사용하고 스프링 Bean으로 만들지만 웹을 담당하는 프레젠테이션 계층은 스프링 외의 기술을 사용하는 경우도 있기 때문이다.
예를들어 웹 서비스 엔진이나 Ajax 프레임워크에서 받은 요청을 스프링으로 전달해서 처리해야 하는 경우 사용할 스프링 Bean은 대부분 서비스 계층이나 데이터 액세스 계층에 속해있다.
이런 경우 때문에 스프링 Servlet을 사용하는 스프링의 웹 기술 외의 다른 웹 기술을 고려중이라면 계층형태로 구분하는 것이 바람직하다.
스프링에서는 웹 애플리케이션마다 하나씩 존재하는 Servlet Context를 통해 RootApplicationContext에 접근할 수 있는 방법을 제공한다.
이를 통해 Ajax 엔진 같은 곳에서 RootApplicationContext에 접근할 수 있다.
WebApplicationContextUtils.getWebApplicationContext(ServletContext sc)
이런 스프링의 간단한 유틸리티 메소드를 이용하면 스프링 밖의 어디서라도 웹 애플리케이션의 RootApplicationContext를 얻을 수 있다.
그리고 getBean() 메소드를 사용하면 RootContext의 어떤 Bean이든 가져와 쓸 수 있다.
ServletContext는 웹 애플리케이션마다 하나씩 만들어지는 것으로, Servlet의 runtime 환경정보를 담고있다.
HttpServletRequest나 HttpSession 오브젝트를 갖고 있다면 간단히 ServletContext를 가져올 수 있는데 스프링과 연동돼서 사용할 수 있는 서드파티 웹 프레임워크는 이 방법을 사용해 스프링 Bean을 가져와 사용한다.
ServletContext에 접근할 수 있는 JSP나 일반 Servlet에서도 가능하다.
프레젠테이션 계층을 분리해서 계층구조로 ApplicationContext를 구성해두면 언제든 간단히 웹 기술을 확장하거나 변경, 조합해서 사용할 수 있으므로 당장에는 스프링 servlet 하나만 존재한다고 해도 계층 구조로 만들어두는것이 권장된다.
계층구조가 만들어지기 때문에 계층구조에서의 주의사항을 항상 염두에 두어야 한다.
계층구조에서는 부모 컨텍스트와 자식 컨텍스트가 동일한 이름의 Bean을 갖고 있다면 자식 컨텍스트의 Bean이 우선된다.
예를들어 hello라는 Bean이 ChildContext와 ParentContext에 동일하게 hello라는 이름으로 존재한다고 하고 hello Bean을 요청했을 때 ChildContext에 존재하는지 먼저 확인한 뒤에 존재하지 않으면 ParentContext에서 찾기 때문에 ChildContext에 있는 Bean을 우선적으로 가져오게 된다.
또한 자바에서 상속받았을 때 처럼 자식 컨텍스트의 Bean은 부모 컨텍스트의 Bean을 참조할 수 있지만 반대로 부모 컨텍스트가 자식 컨텍스트의 Bean을 참조하는것은 불가능하다.
웹 애플리케이션에서 계층구조는 ServletContext와 RootApplicationContext 두가지의 구조가 만들어지고 ServletContext가 자식 컨텍스트, RootApplicationContext가 부모 컨텍스트다.
그럼 Servlet Context의 Bean은 RootApplicationContext의 Bean을 참조할 수 있지만 반대는 불가능하고,
두 컨텍스트에 동일한 이름의 Bean이 존재한다면 RootApplicationContext보다 ServletContext에 존재하는 Bean이 더 우선되어 RootApplciationContext의 Bean은 무시될 수 있다.
하나의 컨텍스트에 정의된 AOp 설정은 다른 컨텍스트의 Bean에는 영향을 미치지 않는다는 점도 주의해야 한다.
스프링 애플리케이션의 XML 설정 파일은 보통 계층이나 성격에 따라 여러개의 파일로 분리해서 작성하면 편리하다.
이런 경우 각 설정파일마다 하나씩 컨텍스트를 만들고 계층구조로 묶는 방법도 가능하겠지만, 반대로 하나의 컨텍스트가 여러개의 설정 파일을 사용하도록 할 수도 있다.
같은 컨텍스트가 사용할 Bean 설정이라면 굳이 파일을 여러개 쪼개서 작성하지 않고 하나에 작성하는 것이 더 낫지 않을까 하는 생각도 할 수 있지만 등록되는 빈의 개수가 많은 경우 파일 하나에 설정정보의 양이 너무 많아지게 되고 작성하거나 관리하는데 불편해지기 때문에 계층별로 구분해두거나 자주 바뀌는 설정과 고정된 설정을 구분하는게 좋다.
2. Web Application의 context 구성방법
웹 애플리케이션의 ApplicationContext를 구성하는 방법으로는 다음 세가지를 고려해볼 수 있다.
1. ServletContext와 RootApplicationContext 계층구조
가장 많이 사용되는 기본적인 구성방법이다.
스프링 웹 기술을 사용하는 경우 웹 관련 Bean들은 Servlet의 컨텍스트에 두고 나머지는 RootApplicationContext에 등록한다.
Root 컨텍스트는 모든 Servlet 레벨 컨텍스트의 부모 컨텍스트가 된다.
스프링 웹 외에도 기타 웹 프레임워크나 HTTP 요청을 통해 동작하는 각종 서비스를 함께 사용할 수도 있다.
2. RootApplicationContext 단일구조
스프링 웹 기술을 사용하지 않고 서드파티 웹 프레임워크나 서비스엔진만을 사용해서 프레젠테이션 계층을 만든다면 스프링 Servlet을 둘 이유가 없다.
따라서 servlet의 ApplciationContext도 사용하지 않게 된다.
이때는 RootApplicationContext만 등록해주면 된다.
3. ServletContext 단일구조
스프링 웹 기술을 사용하면서 스프링 외의 프레임워크나 서비스 엔진에서 스프링의 Bean을 이용할 생각이 아니라면 RootApplicationContext를 생락할 수도 있다.
대신 Servlet에서 만들어지는 컨텍스트에 모든 Bean을 다 등록하면 된다.
계층구조를 사용하면서 발생할 수 있는 혼란을 근본적으로 피하고 단순한 설정을 선호한다면 이 방법을 선택할 수 있다.
이때는 servlet안에 만들어지는 ApplicationContext가 부모 컨텍스트를 갖지 않기 때문에 스스로 Root 컨텍스트가 된다.
이렇게 만들어지는 Servlet context는 컨텍스트 계층 관점에서 보자면 Root 컨텍스트이지만 웹 애플리케이션 레벨에 두는 공유 가능한 Root 컨텍스트와는 구별된다.
이 방법들 중 첫번째 방법은 컨텍스트 계층구조를 만드는 방법이고 나머지 두 방법은 단일구조로 컨텍스트를 하나만 사용하는 방법이다.
첫번째와 세번째 방법은 스프링 웹 기능을 사용하는 경우이고, 두번째 방법은 스프링 웹 기술을 사용하지 않을 때 적용 가능한 방법이다.
3. RootApplicationcontext, ServletApplicationContext 등록
RootApplicationContext를 먼저 정리하고 그 다음 ServletApplicationContext를 정리한다.
웹 애플리케이션 레벨에 만들어지는 Root Web ApplicationContext를 등록하는 가장 간단한 방법은 servlet의 이벤트리스너(EventListener)를 이용하는 것이다.
스프링은 웹 애플리케이션의 시작과 종료시에 발생하는 이벤트를 처리하는 리스너인 ServletContextListener를 이용한다.
ServletContextListener 인터페이스를 구현한 리스너는 웹 애플리케이션 전체에 적용 가능한 DB 연결 기능이나 logging 같은 서비스를 만드는데 유용하게 쓰인다.
이를 이용해 웹 애플리케이션이 시작될 때 RootApplicationContext를 만들어 초기화하괴, 웹 애플리케이션이 종료될 때 컨텍스트를 함께 종료하는 기능을 가진 리스너를 만들수도 있다.
스프링은 이러한 기능을 가진 리스너인 ContextLoaderListener를 제공한다.
사용하는 방법은 아래처럼 web.xml 파일 안에 리스너 선언을 넣어주기만 하면 된다.
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
ContextLoaderListener는 웹 애플리케이션이 시작할 때 자동으로 RootApplicationContext를 만들고 초기화해준다.
리스너에 별다른 파라미터를 지정하지 않으면 default로 설정된 아래의 값이 적용된다.
- ApplicationContext 클래스 : XmlWebApplicationContext
- XML 설정 파일 위치 : /WEB-INF/applicationContext.xml
RootApplicationContext는 웹 애플리케이션의 WEB-INF 폴더 내에 있는 applicationContext.xml 파일을 default 설정 파일로 사용한다.
Context Class와 설정파일 위치는 Servlet Context 파라미터를 선언해서 변경할 수 있다.
ContextLoaderListener가 이용할 파라미터를 <context-param> 항목 안에 넣어주면 default 설정 대신 파라미터로 지정한 내용이 적용된다.
● contextConfigLocation
default XML 설정 파일 위치는 파라미터를 선언해주는 것으로 바꿀 수 있다.
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/daoContext.xml
/WEB-INF/applicationContext.xml
<!-- 하나 이상의 XML 설정 파일을 사용할 경우
여러줄에 걸쳐 넣어주거나 공백으로 분리하면 된다. -->
</param-value>
</context-param>
이렇게 contextConfigLocation context 파라미터를 넣어주면 default 설정파일 위치인 /WEB-INF/applicationContext.xml은 무시되고 파라미터로 제공된 설정파일을 사용하게 된다.
위 예제에 /WEB-INF/applicationContext.xml 도 있는데 왜 무시된다는건가? 라고 생각했었는데 이 설정이 없으면 default 설정파일 위치인 /WEB-INF/applicationContext.xml만을 설정 파일로 사용하는 것이고, 위 예제 처럼 contextConfigLocation 컨텍스트 파라미터를 넣어주면 /WEB-INF/daoContext.xml과 /WEB-INF/applicationContext.xml을 설정 파일로 사용하게 된다.
즉, 위 예제에서 applicationContext.xml을 빼고 다른 xml 설정 파일 경로를 작성한다면 applicationContext.xml 말고 그 파일을 설정파일로 사용하게 된다는 의미다.
설정 파일의 위치는 resource loader가 사용하는 접두어를 사용해 표현할 수도 있으며 접두어를 붙이지 않으면 웹 애플리케이션의 servlet resource path로부터 파일을 찾는다.
그래서 보통 /WEB-INF/로 시작하게 된다.
servlet resource path 대신 classpath로부터 설정파일을 찾게 할 수도 있는데 다음과 같이 classpath: 를 붙여주면 된다.
<param-value>classpath:daoContext.xml</param-value>
ANT 스타일의 경로표시 방법을 이용하면 한번에 여러개의 파일을 지정할 수도 있다.
- /WEB-INF/*Context.xml : WEB-INF 밑의 Context.xml로 끝나는 모든 파일을 지정한다.
- /WEB-INF/**/*Context.xml : WEB-INF 밑의 모든 서브폴더에서 Context.xml로 끝나는 모든 파일을 지정한다.
애플리케이션의 규모가 커져서 등록해야 할 Bean이 많아지면 Bean 설정을 여러개의 파일로 쪼개서 관리하는게 편리할 수 있다.
계층별로 구분하거나 기능 모듈별로 파일을 분리해서 만드는 방법도 좋다.
설정파일이 여러개 만들어졌다고 해서 컨텍스트도 여러개가 만들어지는건 아니고 하나의 Root 컨텍스트가 여러 파일의 Bean 설정 메타정보를 통합해서 사용할 뿐이다.
● contextClass
ContextLoaderListener가 자동으로 생성하는 컨텍스트의 클래스는 기본적으로 XmlWebApplicationContext다.
이를 다른 ApplicationContext 구현 클래스로 변경하고 싶으면 contextClass 파라미터를 이용해 지정해주면 된다.
여기에 사용될 컨텍스트는 반드시 WebApplicationContext 인터페이스를 구현해야 한다.
XmlWebApplicationContext외에 스프링이 제공하는 대체 가능한 컨텍스트 클래스는 AnnotationConfigWebApplicationContext다.
이 클래스는 XML 설정 대신 소스코드 내의 annotation 선언과 특별하게 만들어진 자바 코드를 설정 메타정보로 활용하는 것이다.
이 annotation 설정에 대한것은 이후 포스팅에서 따로 정리한다.
아래 예제는 AnnotationConfigWebApplicationContext를 사용해 RootApplicationContext를 생성하도록 지정한 것이다.
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
AnnotationConfigWebApplicationContext를 contextClass로 사용할 때는 contextConfigLocation 파라미터를 반드시 선언해줘야 한다.
이때는 XML 파일 위치가 아니라 설정 메타정보를 담고 있는 클래스 또는 Bean Scanning package를 지정할 수 있다.
이번에는 Servlet ApplicationContext 등록에 대해 정리한다.
스프링의 웹 기능을 지원하는 Front Controller Servlet은 DispatcherServlet이다.
DispatcherServlet은 이름에서 알 수 있듯이 web.xml에 등록해서 사용할 수 있는 평범한 servlet이다.
servlet 이름을 다르게 지정해주면 하나의 웹 애플리케이션에 여러개의 DispatcherServlet을 등록할 수도 있다.
각 DispatcherServlet은 servlet이 초기화 될 때 자신만의 컨텍스트를 생성하고 초기화 한다.
동시에 웹 애플리케이션 레벨에 등록된 RootApplicationContext를 찾아서 이를 자신의 부모 컨텍스트로 사용한다.
아래 예제는 Servlet ApplicationContext를 등록하는 방법이다.
<!-- web.xml -->
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
DispatcherServlet을 등록할 때 신경써야 할 사항은 다음 두가지다.
● <servlet-name>
DispatcherServlet에 의해 만들어지는 ApplicationContext는 모두 독립적인 네임스페이스를 갖게 된다.
이 네임스페이스는 servlet단위로 만들어지는 컨텍스트를 구분하는 키가 된다.
네임스페이스는<servlet-name> 으로 지정한 servlet 이름에 -servlet을 붙여서 만들면 되는데
예를들어 spring이라는 이름의 servlet의 네임스페이스를 만들면 spring-servlet이 된다.
네임스페이스가 중요한 이유는 DispatcherServlet이 사용할 default XML 설정 파일의 위치를 네임스페이스를 이용해 만들기 때문이다.
Servlet Context가 사용할 default 설정파일은 '/WEB-INF/' + servlet name space + '.xml' 과 같은 규칙으로 만들어진다.
따라서 <servlet-name>을 spring이라고 했다면 default 설정파일 위치는 /WEB-INF/spring-servlet.xml이 된다.
이렇게 servlet 이름을 이용해 네임스페이스를 만드는 이유는 여러개의 DispatcherServlet이 등록되더라도 각각 구분할 수 있고, 자신만의 default 설정파일을 가질 수 있도록 하기 위해서다.
● <load-on-startup>
<load-on-startup>은 servlet 컨테이너가 등록된 servlet을 언제 만들고 초기화 할지, 또 그 순서는 어떻게 되는지를 지정하는 정수값이다.
이 항목을 아예 생략하거나 음의 정수로 넣으면 해당 servlet은 servlet 컨테이너가 임의로 정한 시점에서 만들어지고 초기화된다.
반대로 0 이상의 값을 넣으면 웹 애플리케이션이 시작되는 시점에서 servlet을 로딩하고 초기화한다.
또한 여러개의 servlet이 등록되어 있다면 작은 수를 가진 servlet이 우선적으로 만들어진다.
DispatcherServlet은 servlet의 초기화 작업 중에 스프링 컨텍스트를 생성한다.
컨텍스트의 설정이나 환경에 문제가 있다면 컨텍스트 생성 시 대부분 확인이 가능하기 때문에 웹 애플리케이션이 시작되고 가능한 한 빨리 ServletContext의 초기화가 진행되는 것이 바람직하다.
그래야만 컨텍스트와 Bean의 초기화 작업을 통해 문제를 빨리 파악할 수 있기 때문이다.
<load-on-startup>의 값은 보통 1을 넣어준다.
RootApplicationContext는 서비스 계층과 데이터 액세스 계층의 빈을 모두 포함하고 있고, 그 외에도 각종 기반 서비스와 기술 설정을 갖고 있다.
따라서 설정파일을 여러개로 구분해두고 default 설정파일 위치 대신 <context-param>으로 지정된 설정 파일 위치를 사용하는 경우가 많다.
그에 반해 servlet이 사용하는 설정파일은 굳이 여러개로 구분해서 분리할 필요가 없는 경우가 대부분이다.
따라서 특별한 경우가 아니라면 servlet 이름 + '-servlet.xml'이라는 ServletContext의 default 설정 파일 이름을 따르는 것이 간편하다.
DispatcherServlet의 컨텍스트에 대한 default 설정을 변경하고 싶다면 RootApplicationContext와 마찬가지로 contextConfigLocation과 contextclass를 지정해줄 수 있다.
ServletContext의 파라미터 선언 방법은 RootContext와 거의 비슷하다.
파라미터의 선언에 <context-param> 대신 <servlet> 안에 있는 <init-param> 을 이용한다는 점만 다르다.
Servlet 설정 파일을 default를 쓰지 않고 여러개로 분리해야 할 때가 있는데 RootApplicationContext를 사용하지 않고 모든 계층의 Bean을 ServletContext 안에 등록하는 단일 ServletContext 구성방법을 사용하는 경우다.
웹 계층 외의 Bean을 정의한 applicationContext.xml과 웹 계층을 위한 spring-servlet.xml을 ServletContext가 모두 사용하게 한다면 아래와 같이 선언하면 된다.
이렇게 하면 리스너를 통한 RootContext의 등록은 생략할 수 있다.
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-value>contextConfigLocation</param-value>
<param-value>
/WEB-INF/applicationContext.xml
/WEB-INF/spring-servlet.xml
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
RootContext 설정과 마찬가지로 <init-param>에 contextClass 파라미터를 정의해서 context class도 변경할 수 있다.
'Spring' 카테고리의 다른 글
IoC와 DI(5. Autowiring, ComponentScan) (0) | 2022.08.25 |
---|---|
IoC와 DI(4. Bean 설정 방식, DI 방식) (0) | 2022.08.24 |
IoC와 DI(2. IoC Container의 종류와 사용방법) (0) | 2022.08.18 |
IoC와 DI(1. IoC Container란) (0) | 2022.08.18 |
JPA 양방향 연관관계(mappedBy) (0) | 2022.03.18 |