소통 하고싶은 개발자

프로젝트 진행 ⑤ : 구조 개선 - 어노테이션으로 컨트롤러 만들 수 있는 구조 만들어보기 본문

토이 프로젝트/익명 채팅 사이트

프로젝트 진행 ⑤ : 구조 개선 - 어노테이션으로 컨트롤러 만들 수 있는 구조 만들어보기

OhPro 2022. 7. 9. 11:41
반응형

목차

  • 시작하며
  • 커스텀 어노테이션 만들기
  • 어노테이션 사용할 구조 만들기
  • 어노테이션 사용하기
  • 마치며

 


 

시작하며

저번 포스팅 때 어노테이션으로 컨트롤러를 매핑해줄 수 있도록 시도를 해보겠다고 했었다. 사실 잘 안되면 바로 원래대로 돌려서 작업을 이어가려고 했었다. 그리고 실제로도 좀 어려웠긴 했다 ...^^ 그래도 방향이 안보이면 스프링 프레임워크를 벤치마킹해서 최대한 구현해보려고 했고 구현에 성공했다. 아직까지 에러는 실수성 에러만 있고 고치면서 사용하는 중이다.

 


 

커스텀 어노테이션 만들기

사실 원래는 만들어진 어노테이션만 사용하기에 급급했었다. 예를 들어 @Override 라던지 혹은 롬복에 @Getter, @Setter같은 편리한 어노테이션이 많기 때문이다. 그리고 스프링을 공부하면서도 많이 알게 되었는데 그러던 와중에 어노테이션을 커스텀으로 만들 수 있다는 사실을 알아냈었다. (사실 웹겸으로 자바도 계속 공부하는 중이라서^^)

 

그래서 바로 본론인데, 스프링 MVC에서 @RequestMapping 어노테이션을 구현한다면 이전에 컨트롤러가 너무 많아지는 문제(사실 문제라고 할 수는 없을 수도 있다.)를 해결할 수 있을 것이라는 생각에 어노테이션을 만들어봤다.

 

@RequestMapping

 

스프링에 있는 어노테이션을 벤치마킹했기 때문에 이름도 동일하게 @RequestMapping이라고 지었다. 메서드에 붙힐 수 있고, 런타임까지 어노테이션을 유지하고 있을 수 있도록 설정했다.

 

어노테이션은 조금 특수하지만 인터페이스로 분류하기 때문에 멤버들이 함수형태로 작성되어야 한다고 한다. 커스텀 어노테이션에 대해서는 나중에 더 자세하게 포스팅하고 댓글로 달아두겠다.

 


 

어노테이션을 사용할 구조 만들기

기존에는 사용자 요청이 들어오면 요청에 따라 프로퍼티 파일(.properties)로 미리 매핑해둔 정보를 바탕으로 컨트롤러를 찾았었고, 추상화하여 공통 메서드인 execute()로 컨트롤러를 디스패쳐 서블릿에서 실행시켰었다.

 

그런데 어노테이션을 만약 메서드에 붙힌다면, 메서드 단위로 컨트롤러를 불러올 수 있어야 한다. 그렇게 되면 기존에 디스패쳐 서블릿에서 컨트롤러 클래스를 불러와서 execute()메서드를 실행해주던 구조와 함께 동작될 수 있도록 구조를 바꿔주던가, 아니면 아예 어노테이션 방식으로 바꿔주던가 둘 중 하나의 작업이 필요했다. 

 

나는 프로퍼티 파일을 불러와서 매핑 및 실행하는 과정과 어노테이션을 읽어와서 매핑 및 실행하는 작업을 둘 다 실행 가능한 구조로 만들기로 했다. 그렇게해서 도저히 어노테이션을 만드는게 힘들면 원래 방식대로 진행하려 생각했기 때문이다.

 

내가 생각한 구조는 아래와 같다.

 

구조

 

AnnotationControllerAdapter 생성자

 

먼저 AnnotationControllerAdapter객체를 만들었는데 BaseControllerAdapter를 상속받은 객체이다. 어노테이션을 사용한 컨트롤러의 어댑터 객체라는 의미로 디자인 패턴 중 하나인 어댑터 패턴에서 유래한 것이다.

 

어댑터 패턴을 사용하는 이유는 기존 시스템에 부합하지 않는 것을 부합하도록 하기 위해서라고 하는데 더 자세히는 나중에 포스팅 해보도록 하겠다!

 

어쨌든 여기서 어댑터 객체의 역할은 매핑 객체로 찾은 컨트롤러를 실행하고 뷰 객체(ViewInfo)를 반환하도록 하기 위함이다.

 

처음 생성자를 호출하게 되면 어댑터 객체에서 has 관계로 갖고 있는 RequestControllerMapping 객체를 RequestControllerMappingFactory 객체에서 받아온다. 아래는 RequestControllerMappingFactory 객체의 내부이다.

 

RequestControllerMappingFactory

 

여기서 Factory 객체를 만든 이유는 다음과 같다.

 

  • 어댑터 객체에 따라 매핑방식이 무조건적으로(하드코딩으로) 결정되도록 구성한다면 객체간 의존도가 높아진다.
  • 객체간 의존도가 높아지면 수정해야하는 일이 생겼을 때 범위가 많아져서 힘들다.
  • 현재 어노테이션을 매핑할 때 자바 리플렉션을 사용하도록 구성이 되어있는데 리플렉션은 처리속도가 좋지는 않다고 알고있고, 추후에 AnnotationControllerAdapter객체를 사용하긴 하지만 리플렉션을 쓰지 않을 수 있다면 RequestControllerMapping객체를 바꿔야 할 수도 있고, 그럴 때를 대비할 수 있다.

 

어댑터에서 매핑 객체를 갖고있긴 하지만 매핑하는 방식은 달라질 수 있기에 Factory를 사용하여 매핑 객체를 선택할 수 있도록 해두었다. 그래서 enum을 매개변수로 받아 생성하는 RequestControllerMapping을 선택할 수 있도록 해두었다. enum을 매개체로 삼은 이유는 아래와 같다.

 

  • int 형식을 사용했을 경우엔 해당 int의 의미를 외워야 한다. -> 나중에 머릿속에 과부하가 걸리는 이유 중 하나가 된다.
  • 특별한 상수를 사용했을 경우엔 곳곳에 해당 상수를 다시 작성해줘야 한다. -> 귀찮음
  • String 형식은 괜찮아 보이지만 오타가 발생하면 여러모로 귀찮다.
  • enum은 변수로 사용하면서 ide의 도움을 받아 꽤 간단하게 참조할 수 있고 오타 확률이 적으며 이름으로 의미를 알기 쉽다.

 

이렇게 매핑 객체를 선언해준 뒤 사용자 요청(Request) 컨트롤러(Controller)를 매핑하여 uri 요청에 대비할 수 있도록 entryControllers() 메서드를 사용한다. 해당 메서드는 부모 인터페이스에서 오버라이딩 하도록 했다. 공통적인 기능이기 때문이다.

 

 

위 사진은  entryControllers() 의 내부인데 메서드 실행 과정을 설명하자면 다음과 같다.

 

  1. 프로퍼티 파일에서 기준이 될 컨트롤러 클래스를 불러옴
  2. Class.forName("클래스이름") 메서드로 이름으로 클래스를 선언
  3. Class.getResource("상대경로") 메서드로 빌드 시 해당 클래스가 생성될 곳의 경로를 가져옴
  4. 여기서는 상대경로를 "."으로 지정했기에 해당 클래스가 존재하는 디렉토리(폴더)를 URL 객체로 넣음
  5. URL 객체에서 경로를 가져와 File의 형태로 생성
  6. 가져온 File객체에서 .list() 메서드를 사용하여 해당 디렉토리의 하위 파일들을 가져와 rootPath에 저장
  7. 이전에 구한 클래스에서 패키지 이름을 구해 packageName 변수에 저장
  8. DFS 방식의 완전 탐색함수로 기준 클래스와 같은 경로이거나 하위 경로에 있는 모든 컨트롤러를 탐색하며 매핑 실시

 

여기서는 파일의 기준 경로와 패키지의 기준 경로를 따로 두었는데 왜냐면 파일 경로와 패키지 경로를 표기하는 방법이 다르고(파일은 /, 패키지는 .을 기준으로 나눔), 경로가 시작하는 곳이 다르기 때문이다. 하위 메서드들을 하나씩 살펴보면

 

dfsSearchFromDir

 

맨 처음에 directory.list() 메서드로 얻은 루트 디렉토리(기준 컨트롤러가 존재하는 경로)를 순회하면서 클래스 파일인지 탐색한다. 탐색하는 도중에 이너클래스는 제외하고 만약 디렉토리를 발견했다면 재귀를 사용해 다시 dfsSearchFromDir() 메서드를 실행시킨다.

 

그리고 발견한 파일에 .class가 붙어있다면 지워주고 이름으로 클래스 파일을 찾아준다. convertNameToClass() 메서드의 내부에서도 Class.forName() 메서드를 사용한다. 만약 클래스파일이 아니거나 불러올 수 없는 클래스 파일의 경우에는 null 값을 반환시킨다.

 

그리고 정상적으로 불러온 클래스 파일에 대해서 examinMethods(클래스) 메서드를 실행시키는데, 클래스 내부의 메서드들을 탐색하라는 의미로 네이밍했다.

 

examineMethods

 

내부에서는 메서드들을 for 문으로 순회하며 메서드에서 RequestMapping 어노테이션을 불러온다. getAnnotation() 메서드를 어노테이션 클래스로 찾았을 경우에 찾을 수 없었다면 null 을 반환 시키는 것을 이용해 예외 처리를 했다.

 

그리고 uri를 불러와서 <uri : instance>의 형태로 맵 객체에 저장시킨다. 맵 객체는 private로 데이터에 직접 접근할 수는 없도록 만들었다. 

 

getController

 

이제 어댑터 객체에서 매핑 객체(RequestControllerMapping)의 getController() 메서드를 사용해서 매핑된 컨트롤러를 불러올 것이다. 해당 메서드도 위의 entryControllers() 메서드처럼 부모 인터페이스에서 상속받은 메서드이다.

 


 

어노테이션 사용하기

이제 사용자의 요청이 들어왔을 때 어노테이션의 쓰임새를 살펴보자.

 

execute

 

사용자의 요청이 들어오면 uri를 바탕으로 매핑된 컨트롤러 클래스를 획득하고 획득하지 못했다면 pageNotFound를 반환하여 페이지를 찾지 못했습니다 페이지를 띄워준다. 꼭 페이지 요청이 아닐 수도 있지만 톰캣 오류가 디스플레이 되는 것보단 나을거라고 생각해서 이렇게 해두었다.

 

그리고 getControllerMethod(컨트롤러 클래스, url, 메서드 방식) 메서드를 사용해서 실제 @RequestMapping 어노테이션과 타입이 부합하는 메서드를 검색하고, 찾은 메서드를 invoke() 메서드로 실행하여 Object 형식의 뷰를 리턴받고 리턴 해준다.

 

getControllerMethod()

 

찾은 클래스에서 메서드를 순환하여 uri와 요청메서드가 같은지 비교하여 해당 메서드를 반환해주는 함수이다. 여기서 만약 메서드를 매핑했다면 그냥 메서드를 바로 실행할 수 있지만 굳이 메서드들을 매핑하지 않고 클래스를 매핑한 이유는 invoke() 메서드를 실행하기 위해서 부모가 되는 클래스를 인자로 받아야 하기 때문이다.

 

또한 invoke() 메서드는 인자로 파라미터들을 넣어줄 수 있기 때문에 HttpServletRequest, HttpServletResponse 객체를 반환해줘야 하면 그렇게 할 수 있도록 했다.

 

ObjectToViewInfo

 

invoke() 메서드로 실행하여 얻은 리턴 값을 뷰 형식으로 변환해주기 위해 instanceof 를 사용했다. String 인 경우 뷰 페이지의 의미로 생각하고 ViewInfo로 변환해주었다.

 

이후에는 디스패쳐 서블릿이 ViewInfo를 받아 ViewResolver에게 넘겨주어 실제 뷰 페이지 경로를 불러오고 포워딩 or 리다이렉팅해서 결과 값을 보여준다.

 


 

마치며

구조를 만들고 이전까지 했던 작업들을 지금의 방식으로 옮기는 데 시간을 많이 썼다.. 일단 최대한 일정에 맞추려고 노력해야겠다..! 그래도 구조가 개선되면서 작업 속도가 향상되는 느낌이 들어 괜찮았다.

 

감사합니다!!

반응형