일반적인 서비스에서는 여러 Entity의 다양한 필드들을 유저나 관리자의 API 호출등으로 수정하게 됩니다. 단순히 조회수 등이 추가 되는것이 아니라 도메인의 메인 프로퍼티들이 변경됩니다. 유저의 전화번호나 주소 부터, 병원의 이미지uri 나 설명등 과 같은. Show 이런것들은 보통 Restful 에서 3 같은 API 를 통해 수정이 되게 되는데, 구현 방식에 따라 몇가지 차이가 있을 수 있습니다.
(애초에 DTO 만들어서 Entity 와 분리시켜야 겠지만 현재 레거시를 다 들어내기 힘들다는 가정하에) 아래 코드를 보면 특정 도메인의 필드들을 볼 수 있는데, 5 라는 메소드가 보입니다. 수정 api 를 날리면 5 메소드를 통해서 새로운 값을 반영하도록 서비스단을 공통화 해둔 상태였습니다.
위 코드는 여러 단점이 있는데
그럼에도 불구하고, 작업하려면 모든 클래스의 필드에 직접 접근해야 다보니 쉽게 공통코드화 할 수는 없었습니다. 그러던 어느날, 당시 새로 합류한 카이의 코멘트를 보고 다시금 각성! 띠용! 이럴수는 없어! 해결을 해야겠다!!! 해결 방법 고민하지만 8 에는 9 이 있습니다. 0 내의 필드와 메소드등에 접근해서 활용할 수 있게 해주는 이 util 을 이용해서 자동화를 도전해보고 싶어졌습니다.요구사항은 2가지
클래스의 필드를 잔뜩 가져오는 것은 리플렉션으로 해결한다면, 그 중 특정 필드만 수정 가능하다는 체크는 어떻게 할것인가? 바로 1 을 활용! Custom annotation 을 만들어서 해당 필드에 달아주고, 그 필드들만 대상으로 merge 를 진행해보려 합니다.일단 리플렉션부터 시작해봅니. Reflection 을 이용한 필드 접근아래는 리플렉션을 알아보기 위해 짜 본 테스트 코드
Class 의 2 를 통해 private field 들을 가져올 수 있고, 각 필드 메소드를 간단 소개하자면
수행 결과는 아래와 같습니다.
자 이제 어느정도 이해가 되었으니, 어노테이션을 제작해봅니다. Custom Annotation 제작 및 적용
위와 같이 8를 만들면 어노테이션이 생성됩니다.안에 있는 필드 9 은 추후 어노테이션의 프로퍼티로 사용될 수 있습니다. null 을 무시하지 않는 케이스를 위해 만들어 두었습니다.다시 TestCase 로 돌아가서 이번에는 어노테이션에 0 를 넣어보겠습니다.
위에 처럼 짜서 돌려보면 annotation 값에 1 라는 어노테이션이 달려있을때만 해당 어노테이션 객체가 들어오게 됩니다. 그러면 저렇게 들어오는 필드일때만 비교해서 넣어주고, 아니면 무시하고! 하면 첫번째 요구사항이 해결됩니다.1.특정 필드만 수정(merge) 대상이어야 한다. 그럼 이제 2이 필요합니다.
두 값을 비교해서 source 값이 null 이 아닌 경우에만 equals 로 동일 체크를 해서 다르면 update 대상임을 알려줍니다. (위)
이렇게 처리하고 필요할때마다 3 로 해주면 분기가 가능합니다. 두번째 요구사항도 해결방법이 보입니다.2.어떤 필드는 예외적으로 null 을 던지면 null 로 바뀌어야 한다 (가끔 이런 케이스가 있음) 위 2가지 메소드와 4 을 이용해 2개의 오브젝트에서 0 붙은 필드들을 찾아내 값을 변경해주는 메소드를 구성합니다.아래에 소스별로 주석을 달아두었으니 참고해주시길. Merge 메소드 (code#1)
이렇게 해서 완성!! 아까는 나오지 않았던 6 값을 7 해주는 메소드가 나왔는데, 대상 8 와 value 만 넣어주면 됩니다.실제로 테스트 케이스에서 다시 시도해봅니다. (code#2)
아래 3개(b,c,d)만 0가 붙어으니 a 필드는 서로 달라도 안 바뀌어야 하며, c 는 null 무시하고 d 는 null 이 되어야 한다.그러니 결과는 0 이어야 하고, Test 케이스는 모두 통과했다!!! 성공!!이제 이 메소드를 잘 활용만 하면 끄읏!!! 아까 다시 TestDomain 클래스 로 돌아가서 바꿔준다면? (code#3) 마치며오래 전 부터 9 으로 어떻게 해볼수 있지 않을까 상상만 하다가 그쳤었는데, 2 활용까지 더해서 구현을 마칠 수 있었습니다. 예상보다 막히는 부분이 없어서 빨리 끝날 수 있어서 좋았구요. (테스트 코드부터 단위별로 짜면서 시작해보았는데.. 역시 TDD 인가!?)예전부터 구상만 하던 거였는데 새로 입사하신 분이 이 레거시 코드의 불편함을 재기해주면서 확 삘 받아서 작업에 들어갈 수 있었습니다. 역시 새로운 사람과 새로운 환경에서의 새로운 자극이, 가끔은 기존에 습관이나 패턴에 익숙해진 우리들을 깨우치고 다시 바꿔나갈 수 있게 해주는것 같아서 매우 좋은 기회였습니다. 마치 3년 묵은 변비를.... 여기까지. fin. 슬프게도 3 를 통해서 필드를 가져오는 경우, 4 의 필드는 가져오지 못한다는 것을 깨달았고**(private 이기 때문에)**, 결국 merge 메소드를 사용할때 슈퍼 클래스의 필드가 머지되지 않는 오류가 있었습니다..ㅠㅠ해결방법 다시 고민3번째 요구사항이 생겼습니다.
그래서 찾아보니 역시 9 안에는 super class 에도 접근할 수 있는 방법이 있습니다. 0흠 그런데 만약 상속이 여러번 중첩되고 한다면... 6 하게 가져올 수 는 있겠지만 뭔가 점점 복잡해지는 상황이 옵니다 아아악..ㅠ그러던 순간, 새로운 방법을 발견했습니다. 7 이 제공해주는 Util 에서 존재하는 모든 필드를 접근해서 수행하는 메소드가 있다는 사실!?!
Access to private inherited fields via reflection in Java Spring has a nice Utility class ReflectionUtils that does just that: it defines a method to loop over all fields of all super classes with a callback: ReflectionUtils.doWithFields() 자세한 건 위의 링크를 보시면 아시겠지만 (스택오버플로우는 사랑입니다) 결국 저 메소드를 사용해서 필터부분과 필드접근부분을 잘 짜면 해결이 가능해 보인다는 사실. 그래서 해보았습니다. To-be Util.merge method(code#1 → code#4) 1그리고 기존 #code2 부분에 있는 Test 코드에 이어서 한가지 테스트케이스를 더 추가해봤습니다. 기존 8 라는 클래스를 상속받아 쓰는 9 를 만들고, 해당 클래스의 오브젝트를 2개 만들어서 머지할 때, super class 인 8 의 필드들도 합쳐지는지를 보는 테스트입니다. |