안녕하세요, Brad입니다. 약 3일동안 틈틈히 '웹 서버'를 만드는 미션을 진행해봤는데요. 최근 손으로 기록하면서 메모를 하고 그게 익숙하지 않아 매일 정리하지 못했네요. 오늘 step1 PR을 끝냈는데요. 진행하면서 고민되었던 부분, 그리고 해결한 내용에 대해 정리해볼게요.
기존에 Spring 프레임워크를 사용하다가 막상 프레임워크 없이 코드를 짜려고 하니 정말 막막하고 여기저기 infra 코드들이 섞이다보니 정말 보기 좋지 않더라구요. 그래서 사실 step1은 다른것 구현없이 요구사항에 맞춰서 진행하면 되는데 욕심내면서 Spring프레임워크를 만드려고 했습니다. 물론 구현하는 것은 대충이나마 큰 문제가 없었지만 효율성 측면이나 복잡한 코드에 대해 자동화를 해주는 부분에 취약점이 여러곳에서 드러나 많은 문제가 있었습니다. 정리할 내용도 Spring프레임워크 같이(?!) 보이게 하는데 많이 고민했습니다.
쿠키값 활용 관련
- 요구사항은
logined=true
로 저장하고 이후에 쿠키값을 읽어 그에 대응하는 처리하는 것이 있었습니다. - 좀더 욕심을 내어 위와 같이 단순한 형태가 아니라 '
user
객체가 들어가면 어떨까' 하는 생각을 했습니다. Java Reflection을 이용하여 매개변수에HttpSession
이 있으면 Request로 들어오는 쿠키값에 주입시키도록 만들긴 하였습니다.
Cookie 값을 Java 객체로 어떻게 만들것인가?
- 문제는
User
타입이라는 것을 어떻게 아느냐 입니다. 왜냐하면 Request에서 들어오는 값은String
이거든요. 그리고 Cookie안에 여러 값을;
을 기준으로 넣을 수 있는데 다른 값들은 어떻게 Java 객체로 만들건지 많은 고민이 되었습니다. - 결국 이 부분은 고민을 많이 하였지만 일단
String
값으로 받아들이는 것으로 만들었습니다.
- 요구사항은
MappingHandler 구현
'user/create' path로 오는 컨트롤러를 구현해야 했을 때 서블릿과 같이 적절한 컨트롤러와 Mapping해주는 역할의 필요성을 느끼고 구현하고 싶었습니다.
그러기 위해선 MappingHandler에 'user/create'와 같은 path값을 가지고 있는 메서드를 다 알아야했습니다. 그래서 사용한 것이 Reflection과 Annotation입니다.
우선 Reflection도 Controller라는 것과 어떤 path와 RequestMethod를 Mapping해야 할지 알아야했기 때문에
@Controller
와@RequestMapping
를 정의해두어야 했습니다.그리고 Map자료형을 static변수로 static블록 안에서 init해주는데 이때는 Reflection을 이용하여
@Controller
내@RequestMapping
내에 value들을 가져와서 넣어 구현하였습니다.Map의 key와 value를 무슨값으로 할까 고민을 했었는데요. 아래와 같이 하는게 좋을 것 같더라구요.
- key는 Url객체가 있는데 이 안에는 상태값으로 AccessPath('user/create'와 같은), ReqeustMethod('GET'과 같은)로 두고 이 두 가지가 같을 때 해당 요청으로 처리해하였습니다. 당연한 것이죠.
- value는
Method
타입으로 해두었습니다. 왜냐하면Method
타입으로 하는 것이 해당 메서드를 특정시킬 수 있고invoke()
하기에도 용이하기 떄문입니다.
순서
@Controller
클래스를 모두 가져옵니다.- 해당 클래스 내에 정의된 메서드들을 가져옵니다.
- 메서드에
@RequestMapping
어노테이션이 있는것을 찾고 이 안의 value와 method를 찾습니다. - 해당 value와 method로 Url객체를 만들어 key로 넣고 해당
Method
객체는 value로 넣어둡니다.
QueryString 처리
- GET방식으로 가져오면 accessPath뒤에 ?가 나오고 이후 &키=값 형태로 뒤엉켜서 값들을 가져오게됩니다. 반면 POST방식은 body안에 넣어서 가져옵니다.
- 처음에 GET방식만 고려했을 때는 처음 줄에
GET /user/create?userId=javajigi&password=password&name=%EB%B0%95%EC%9E%AC%EC%84%B1&email=javajigi%40slipp.net HTTP/1.1
이런 식으로 값이 날라오기 때문에Url
객체 내에 파싱된 QueryString의 값을 Map으로 넣어보관하였습니다. - 하지만 이후에 POST방식은 Header가 다 끝나고 QueryString값을 받을 수 있기 때문에 Url 내 QueryString을 별도로 업데이트해줘야 했습니다.
- 저는 Header값들을
Header
라는 객체에 넣어두고Url
객체는Header
의 인스턴스 변수로 계속 보관했었는데요. 아무래도Url
내에 AccesPath, RequestMethod, QueryString 변수들이 많이 사용되는 값이다보니Header
객체 내에 중간다리 역할을 하는 메서드들을 많이 만들 수 밖에 없었습니다. 이 부분에 대해선 차라리 Header로 값들을 다 올려서Url
객체를 안 만드는게 나았었는지 고민이 되더라구요. - QueryString이 안들어오는 요청의 경우 null값 대신 빈 HashMap이라도 만들어서 NullPointException발생하는 것을 피하려 노력했습니다.
객체 Binding 처리
스프링 프레임워크에 보면 form태그로 보낸 값들이 Controller내 매개변수로 받을 때 값이 다 채워져 있는 것을 알 수 있습니다. 이 부분에 대한 구현도 해보고 싶었습니다.
순서
MappingHandler 내 요청Path에 대한 Method 내에 매개변수들을 확인합니다.
매개변수 내 클래스를 일일이 확인하면서 해당 매개변수 타입의 필드들이 QueryString으로 들어오는 필드값과 모두 같은 타입을 찾습니다.
- 여기서 처음에 오류를 범했었는데요. 처음에 저는 매개변수 타입을 기준으로 QueryString이 들어갈 수 있는 타입을 찾았습니다. 하지만 이 경우 일부 필드값만 form태그로 들어왔을 때 해당 매개변수 타입을 찾지 못하는 오류가 있더라구요.
- 예를들면 이런 상황인거죠.
User
는 userId, password, email, name의 필드를 가집니다. 회원가입할 때는 이 모든 정보가 다 들어있기 때문에 문제가 없지만 로그인 할 때는 userId와 password 값만 QueryString으로 주어집니다. - 이 경우 매개변수 타입을 기준으로 하면 password와 email이 없기 때문에
User
타입이 아니라고 판별해버립니다. 그렇기 때문에 QueryString을 기준으로, 즉 password와 email이User
타입의 필드에 속하는지를 판별해야합니다.
이렇게 모두 맞는 파라미터 타입을 만나면 해당 클래스의 setter메서드를 찾아서 값을 주입시킵니다.
처음에 이것을 AOP로 해야할지 생각했습니다. 하지만 Java Reflection의 Proxy를 이용한 AOP는 인터페이스가 구현되어있어야 가능한데 모든 컨트롤러 메서드에 대한 인터페이스를 만드는건 아닌 것 같아 고민을 하다가 역시나 Reflection만으로 가능하다는 것을 알게되었습니다. 왜냐하면 제가 QueryString값과 해당 Method를 다 알고있기때문입니다.
Controller메서드 invoke()
Mapping과 Parameter Binding까지 끝냈으면 이제 컨트롤러 메서드를 실행하기만 하면 됩니다. 그런데 invoke()할때 해당 매개변수 값들을 어떻게 전해줘야할지 잘 모르겠더라구요.
invoke를 할 때 첫번째 매개변수로 해당 클래스의 인스턴스이고, 두번째는 메서드의 매개변수입니다. 테스트해보면서 Object[] 형태로 전해주면 되더라구요.
- 단, Object[] 은 해당 파라미터의 객체가 있어야하며 순서와 개수가 딱 맞아야합니다!
이전에 Binding하면서 각 파라미터마다 객체를 생성해야했기 때문에 객체로 만들어서 전해주는 것은 크게 힘들진 않았습니다.
View핸들러 어떻게 구성할까?
가장 어려웠던 부분이 View를 만들어주는 것이었습니다.
가장 고민되었던 부분
- 파일 입출력 - 우선 이 부분을 많이 안해봐서..
Model
객체를 만들고 Map<String, Object> 에 값을 넣고 나서 나중에 꺼낼 때 이게 어떤 타입인지 어떻게 알 수 있을까요?(가장 고민되던 부분)
기본적으로 200코드, 300코드가 Header작성하는 부분과 Body작성하는 부분이 공통적으로 나뉘기 때문에 인터페이스를 만들어서 상속 구조가 되도록 하였습니다.
나중에 200코드 내에서 여러 분기처리를 하여야 하는 경우가 있었는데요. 이는 깔끔하게 처리하지 못해 아쉬움이 남습니다.
- 캐쉬를 반영해야할 경우
- css파일을 보내야할 경우
- contentLength가 바뀌는 부분 - 이 부분에서 자꾸 오류가 나는데 왜 그런지 모르겠습니다..
쿠키값 주입
HttpSession이라는 매개변수를 컨트롤러 메서드에서 만들면 쿠키값이 전해지고 있을 때 주입시켜야 합니다.
이는 이전에 QueryString 값 주입할 때와 마찬가지로
instanceof HttpSession
을 이용하여 전해진 쿠키값이 주입되도록 하였습니다.HttpSession
을 부르면 해당 HttpSession을 쓰는지 또 값을 넣는지 알 수 없기 때문에 컨트롤러 메서드가 종료되고 다시 HttpSession객체를 찾아Header
내 Cookie에 값을 집어넣도록 하였습니다.- Map자료형이기 때문에 key값이 같으면 value를 덮어쓰기 때문에 값이 갱신되면 바꿀 수 있고 없었다면 새로 반영할 수 있습니다.
Header
내에boolean
타입으로 HttpSession이 매개변수로 있으면 변화가 있다는 것을 표시하여 이후 200코드 보낼 때 Header에 Cookie값을 기록하도록 하였습니다.아직 풀리지 않은 의문은 처음에 쿠키값이 처음엔 잘 안찍히다가 새로고침 몇번하여야 반영된다는 것입니다. 이 부분은 좀 더 해결해봐야할 문제입니다.
자꾸 찍히고 안찍히고 반복되다보니 계속 쿠키값을 찍도록 반영해보기도 했습니다. 하지만 이렇게 하면 contentLength오류가 자꾸 뜨더라구요. 쿠키값은 한번만 찍어도 잘될때는 계속 되는것 보면 한번만 찍어서 반영하는게 맞는 것 같아요. 물론 변동이 있으면 다시 적어야하기 하지만요. 근데 저 contentLength는 뭔지 너무 궁금하네요!!
step1 PR을 보내고 안 사실이지만 바로 다음 칸에 이것에 대한 동영상 강의가 있더라구요. 혼자 오버한 것 같다는 생각이.. 그래도 며칠동안 프레임워크인 것처럼(?!) 만들어보면서 큰 재미를 볼 수 있었습니다!!
'TIL' 카테고리의 다른 글
Today's Dev Notes(2019-01-23) (0) | 2019.01.23 |
---|---|
서버 N대 성능개선 (0) | 2019.01.22 |
Today's Dev Notes(2019-01-17) (0) | 2019.01.17 |
Today's Dev Notes(2019-01-16) (0) | 2019.01.16 |
Today's Dev Notes(2019-01-15) (0) | 2019.01.15 |