그 동안 웹서비스를 개발해오면서 많은 실수들을 했고 그럴 때마다 다시 처음 부터 시작한다면 이렇게 해보는게 좋을 것 같아…라고 생각 했던 것을 정리해 본다. Back-End 위주의 정리이다.
나의 생각은 경험이 증가하고 기술 환경이 변화함에 따라 함께 계속 변화한다.
단점
, 주의할 점
, 위험성
, 실수
, cons
, pitfall
, gotchas
, warns
, when not to use
, should not use
, avoid
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)에서 상위 계층을 사용하지 말라. 대략 다음과 같은 레이어가 있다고 할 때(위에 있을 수록 상위 레이어) 상위 레이어는 하위 레이어를 사용할 수 있지만 하위 레이어는 상위 레이어의 존재를 몰라야 한다.
항상 의존성은 위에서 아래로 흘러야 한다.
Thread Local
을 사용함)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를 날릴 수 있다.update
,delete
)도 where
조건이 없으면 실행 불가로 하는 것이 좋다.number(BigDecimal)
로 못해도 long
으로 일괄 적용한다. integer
로 할 경우 가격 overflow 에 시달리거나, 나눗셈 등의 연산에서 취약해질 수 있다. DECIMAL(19,2)
추천.0
인 상태와 NULL
인 상태 모두에 대해 항상 조건을 걸거나 NULL → 0
변경을 수행해야만 하게 된다. 또한 boolean도 상태가 true/false/null
세가지 상태가 되어 버린다.null
이 의미가 있다면 정확하게 주석을 단다.createdAt
, modifiedAt
).offset/limit
조회는 offset
이 뒤로 갈수록 성능이 계단형으로 느려지게 된다. offset
을 계산하려면 그보다 앞에 있는 데이터들의 조건을 모두 검사해야하기 때문이다. offset/limit
은 페이징 중에 데이터가 추가 될 경우 그 다음페이지에서 앞에 나왔던 데이터가 다시 나와 중복처리되는 문제가 발생할 수 있다. UI 에서는 큰 문제가 안되지만 batch 나 기타 내부 처리용 데이터 조회시에는 심각한 문제가 될 수 있다.order by PK asc
를 해줘서 페이징의 순서를 명확히 해줘야한다.nextPk,limit
을 조회 조건으로 거는 더보기 방식을 사용해야 성능저하 없이 균일한 응답성을 보장받을 수 있다.BIGINT AUTOINCREMENT
). Insert 시에 PK를 기준으로 지속적으로 물리적 정렬를 하기 때문에 정렬되지 않은 PK insert 가 발생하면 성능이 저하된다. 만약에 비즈니스키가 증가하는 값이 아니라면 항상 인공키로 BIGINT AUTOINCREMENT
로 잡아준다.findAny()
같은것도 유사한 문제를 일으킨다.in
조건 등의 쿼리 생성시 항상 in
안에 들어오는 값의 갯수가 제한될 수 있도록 한다. in
절은 중복을 제거하고 결과를 반환하지만 in
의 키를 분리조회 할 경우에는 중복 조회가 발생하지 않게 조심해야 한다. 미리 중복을 제거하고 조회한다.true/false
로 둘 수 없고 PK 와 동일 값으로 지정되면 삭제된걸리 간주하는 식으로 해서 unique index 를 걸수도 있긴하지만 너무 복잡하고 개발자들이 자주 실수한다.OutOfMemory
를 일으키기도 한다.expire
등에 대해 전 서버가 일제히 대응이 가능해져 일관성이 보장 될 수 있다.<>
) 기반의 커스텀 태그를 사용하는 템플릿 엔진은 사용하지 말라. HTML과 템플릿 코드가 섞여보여서 유지보수성이 현저히 저하된다(JSP, Freemarker 등 쓰지 말라는 얘기).우리는 대박 할인해요 >.< !!
라는 데이터가 있을 때 이 데이터의 근본 형식은 TEXT 이다.>.<
등이나 HTML 태그가 입력될 수 있다고 해서 원천 데이터 저장 자체를 HTML Escape 해서 저장하고자 하는 욕구를 느낄 수 있는데, 절대해서는 안된다.<, > &
같은 문자열들이 노출되게 된다.한글,영문 대소문자, 숫자, _, …
식으로 whitelist 를 지정하는게 낫다.log.info(“{}”, message)
log.info(“주문 {} 취소”, orderNo)
→ log.info(“주문 취소건 orderNo: {}”, orderNo)
주문 취소건
이라는 검색어로 주문 취소와 관련된 연관 로그를 한 번에 검색 가능해진다.@PostConstruct
로 초기화??)pk > 10 limit pageSize
로 그 다음 조회를 한다.limit
이 10이면 11개를 쿼리해서 결과가 11개가 나오면 다음페이지가 존재하고, 아니면 여기서 끝인 것으로 판단하면 된다.offset
/limit
방식도 전체 카운트를 하는 Paging 보다는 낫지만 offset 값이 커질 수로 조건에 맞는 그 앞 데이터에 대한 카운팅을 해야해서 성능이 점점 느려진다. offset
/limit
은 요청자 측에서 페이징 방식으로 자기네가 알아서 감싸는 것이 쉽다. 즉, 두가지 방식을 모두 요청자측에서 결정해서 할 수 있다.4xx
, 5xx
응답 권장).200
으로 정상으로 내리면서, 응답 데이터에 오류 코드등을 넣게 설계하는 경우가 있는데null
혹은 기본값으로 채워지게 되고, 이를 가지고 정상응답으로 착각하고 나머지 프로세스를 타면 아주 치명적인 문제가 될 수 있다.PUT
으로 전체 데이터를 수정하는 것은 MSA 에서는 문제가 될 수 있다. 수정 대상 Entity 에 필드 변화가 생겼을 때 이를 호출하는 서비스에서 그 필드 변경을 제대로 인식하지 못하고 PUT
으로 전체 데이터 수정 요청시 신규 필드의 데이터 유실이 발생할 수 있다.String
으로 받아서 불필요하게 의존성이 안생기게 하는게 낫다.new IllegalArgumentException(“잘못된 전화번호”)
가 아니라 new IllegalArgumentException(String.format(“사용자 %s의 전화번호(%s)가 잘못되었습니다”, userId, phoneNumber))
형태로 구성한다. 실무에서 예외가 발생했을 때 조금이라도 정확하고 빠르게 대응 가능해진다.@ControllerAdvice
, @ExceptionHandler
등을 사용.)+- month
, +- year
로직으로 짜야지 +- 30
, +-365
이런식으로 작성하면 안된다.10:00~10:00
, 10:00~03:00
를 어떻게 해석해야할까? (다음날 10시, 다음날 3시 등의 표현)10:00~11:00
인데 데이터 입력자의 의도가 다음날 11:00
인 경우가 발생할 수도 있다.LocalDateTime.now()
같은 것을 하나의 비즈니스 로직 흐름에서 여러번 호출해서는 안된다. 호출 순서에 따라 시분초 중이 시가 바뀌가나 아예 날짜가 바뀌어버릴 수 있다. 하나의 비즈니스트랜잭션에서는 이 값을 로직의 최초 시작점 혹은 호출자측에서 현재시간을 구해서 인자로 넘겨주고 그 후속 로직은 모두 이 인자를 사용해야 한다. 그렇지 않으면 하나의 트랜잭션안에 현재시간을 나타내는 값이 모두 다르게 들어가서 장애가 날 수 있다.docker-compose
를 통해 실제 서비스를 local 에 띄워서 처리하게 한다.git rebase
하는 습관이 있는 개발자들로 인해 지속적인 conflict 가 발생한다.