문서의 이전 판입니다!
그 동안 웹서비스를 개발해오면서 많은 실수들을 했고 그럴 때마다 다시 처음 부터 시작한다면 이렇게 해보는게 좋을 것 같아…라고 생각 했던 것을 정리해 본다. Back-End 위주의 정리이다.
나의 생각은 경험이 증가하고 기술 환경이 변화함에 따라 함께 계속 변화한다.
ecommerce
프로젝트가 있다고 할 때, 프로젝트를 업무 도메인 단위 모듈로 만든다. 모듈만 분화 됐을 뿐 API 호출이 아닌 실제 코드를 직접 호출하는 방식의 Monolithic Architecture로 구성한다.ecommerce-project
: 이커머스 프로젝트 - 크게 비즈니스, 유틸리티, 사용자 접점 세가지 종류의 모듈로 구성한다.product
: 상품 도메인 비즈니스 모듈order
: 주문 도메인 비즈니스 모듈delivery
: 배송 도메인 비즈니스 모듈account
: 사용자 계정 도메인 비즈니스 모듈admin-account
: 관리자(내부 사용자) 계정 도메인 비즈니스 모듈email-sender
: 메일 발송 모듈. 비즈니스는 아니지만 별도 분할해 두어 추후 기능 확장에 대비user-web
: 일반 사용자가 사용하는 웹 서비스 모듈. 위에 있는 도메인 비즈니스/유틸리티 모듈들에 의존한다.user-app-api
: 일반 사용자 App 에서 호출하는 API 모듈. 위에 있는 도메인 비즈니스/유틸리티 모듈들에 의존한다.admin-web
: 관리자 웹 서비스 모듈. 위에 있는 도메인 비즈니스/유틸리티 모듈들에 의존한다.ecommerce-core
혹은 ecommerce-common
형태의 여러 도메인 비즈니스 로직을 모아둔 공통 모듈을 만들면 절대로 안 된다.interface
와 구현체로 만들고 도메인 로직을 호출하는 측에서는 항상 interface
기반으로 소통한다.interface
기반으로 약한 결합도 높은 응집도를 유지해야 한다.interface
기반이란 단순히 Java의 interface/class 분리를 뜻하는 것이 아니라(그렇게 하는 것도 매우 좋음), 구현의 구체적 정보가 담기지 않은 설계를 의미한다.EmailService.sendEmail(String smtpServer, String userName, String password, String subject, String contents, String from, String to)
smtpServer,userName,password
는EmailService.sendEmail(String subject, String contents, String from, String to)
하위 계층(Layer)에서 상위 계층을 사용하지 말라. 대략 다음과 같은 레이어가 있다고 할 때(위에 있을 수록 상위 레이어) 상위 레이어는 하위 레이어를 사용할 수 있지만 하위 레이어는 상위 레이어의 존재를 몰라야 한다.
특히 모듈화가 잘 안된 프로젝트에서 저지르는 흔한 실수 중의 하나가 로그인 사용자 객체를 자동으로 도메인 객체에 넣어주고 싶다던지의 이유로 Web Layer의 객체를 Domain Object에 넣는 경우가 있는데 이렇게 하면 Batch 등 전혀 다른 목적에서 Domain Object를 사용할 때 엉뚱한 값이 주입되는 등 심각한 부작용이 발생한다.
항상 의존성은 위에서 아래로 흘러야 한다.
user-web
모듈의 기능별 URL Path 를 다음과 같이 만들면/accounts/*
: 계정 로그인, 계정 관리/products/*
: 상품 정보 열람/orders/*
: 주문 처리user-account-web
는 /accounts
담당, user-products-web
는 /products
담당 , .. 등의 형태로 모듈 분할이 어느정도 쉽게 가능해지게 된다.https://www.example.com
, https://www.example.com/account
, https://www.example.com/articles
, 정적 리소스는 https://static.example.com
example.dev
, 통합 테스트 도메인 example.test
처럼 전혀 다른 최상위 도메인으로 끝나게 하면 실서비스와 쿠키 정보가 섞이지 않아 편해질 수 있다. 또한 실서비스에서 테스트인 줄 착각하는 일도 줄어든다.swappiness=0 혹은 1
정도로 설정한다.now()
, password()
같은 것 사용금지. 데이터 생성/변형은 애플리케이션에서 일관되게 처리한다. 그렇지 않으면 추후 확장시 문제요소가 될 수 있다.hbm2ddl.auto
옵션에 의해 DB를 날릴 수 있다.number(BigDecimal)
로 못해도 long
으로 일괄 적용한다. integer
로 할 경우 가격 overflow 에 시달리거나, 나눗셈 등의 연산에서 취약해질 수 있다. DECIMAL(19,2)
추천.0
인 상태와 NULL
인 상태 모두에 대해 항상 조건을 걸거나 NULL → 0
변경을 수행해야만 하게 된다. 또한 boolean도 상태가 true/false/null
세가지 상태가 되어 버린다.createdAt
, modifiedAt
).expire
등에 대해 전 서버가 일제히 대응이 가능해져 일관성이 보장 될 수 있다.<>
) 기반의 커스텀 태그를 사용하는 템플릿 엔진은 사용하지 말라. HTML과 템플릿 코드가 섞여보여서 유지보수성이 현저히 저하된다(JSP, Freemarker 등 쓰지 말라는 얘기).pk > 10 limit pageSize
로 그 다음 조회를 한다.limit
이 10이면 11개를 쿼리해서 결과가 11개가 나오면 다음페이지가 존재하고, 아니면 여기서 끝인 것으로 판단하면 된다.offset
/limit
방식도 전체 카운트를 하는 Paging 보다는 낫지만 offset 값이 커질 수로 조건에 맞는 그 앞 데이터에 대한 카운팅을 해야해서 성능이 점점 느려진다. offset
/limit
은 요청자 측에서 페이징 방식으로 자기네가 알아서 감싸는 것이 쉽다. 즉, 두가지 방식을 모두 요청자측에서 결정해서 할 수 있다.new IllegalArgumentException(“잘못된 전화번호”)
가 아니라 new IllegalArgumentException(String.format(“사용자 %s의 전화번호(%s)가 잘못되었습니다”, userId, phoneNumber))
형태로 구성한다. 실무에서 예외가 발생했을 때 조금이라도 정확하고 빠르게 대응 가능해진다.