TIL - 0625

java-ims

layerd architecture - 계층을 만들어놨으면 이용하기

  • 계층을 만들어놨으면 뛰어넘으려고 하지말고 해당 계층을 거치도록 코딩하기
    • 아래 코드의 상황은 IssueController에서 Milestone의 정보를 얻어야하는데, 곧바로 MilestoneRepository로 가지말고 서비스 계층을 거치도록(직접적으로 사용하지말자 - 중간 계층이 상황에 따라 요청에 따른 응답 결과를 달리할 수 있도록)
@Controller
@RequestMapping("/issues")
public class IssueController {

    @Autowired
    private MilestoneService milestoneService;

    @GetMapping("/{id}")
    public String show(@PathVariable Long id, Model model) {
        model.addAttribute(getEntityName(ISSUE), issueService.get(id));
        model.addAttribute(getMultipleEntityName(MILESTONE), milestoneService.get());
        return String.format("/%s/show", getEntityName(ISSUE));
    }
}

통합테스트(AcceptanceTest) 할 때, 테스트 할 때

  • 클라이언트가 항상 브라우저가 아니라는 생각을 해야함 : 악의적인 뜻을 가지고 요청을 날릴 수 있음
    • 악의적인 상황에 대비해서 예외처리를 해둬야함 : (브라우저라면) 그럴 수 없을거야 라는 생각을 해서는 안됨 - 모든 상황에 대해 예외처리를 해뒀는지 테스트 해야함
@Test
public void selectLabel() {
    ...    
}

@Test
public void selectLabel_fail_unAuthentication() {
    ...
}

@Test
public void selectLabel_fail_invalid_issue_id() {
    ...
}

@Test
public void selectLabel_fail_invalid_label_id() {
    ...       
}
  • 테스트가 귀찮다는건 모든 것을 만들어두고 테스트를 돌리니깐… 테스트 중요성을 모르니깐 : 케이스를 만들어두면 두고두고 테스트를 돌릴 수 있음(한꺼번에!), 수정될 때마다 영향을 미치는지 곧바로 알 수 있음, 테스트 주도 개발하는 습관을 계속해서 만들도록(먼저 케이스를 생각해두고 실제 개발할 때 커버를 하기위해 개발하면 훨씬 더 수월함 - 기획부터)

서버측에서 유효성(validation) 체크

  • asda
    • 예외 체크를 할 때 BindingResult가 메소드에 꼭 드러나야하는지 없앨 수 있는지 찾아볼 생각 : 커스텀 파라미터 어노테이션을 만들면 되지않을까 생각되어서 시도해볼 것
    • 에러가 발생했을 때 요청받은 URI를 파싱해서 다시 redirect 시켜주도록 하였음 : Servlet API를 직접 사용하는데, 래핑된 WebRequest(Spring API)를 사용하려고 했더니 URI를 가져오기위해서는 description 문자열에서 파싱을 해야해서 번거로워 Servlet API를 사용
/* Dto */
public class MilestoneDto {
    private Long id;

    @Size(min = 3, max = 20)
    private String subject;

    @NotNull
    private LocalDateTime startDate;

    @NotNull
    private LocalDateTime endDate;
}

/* 메소드 예외 체크 */
@PostMapping
public String create(@LoginUser User loginUser, @Valid MilestoneDto milestoneDto, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        throw new IllegalArgumentException();
    }
    milestoneService.create(loginUser, milestoneDto);
    return "redirect:/milestones";
}

/* 예외 통합 처리  */
@ControllerAdvice
public class ValidationControllerAdvice {
    private static final Logger log = LoggerFactory.getLogger(ValidationControllerAdvice.class);

    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.FOUND)
    public String handleValidationException(HttpServletRequest request) {
        log.debug("request URI : {}", request.getRequestURI());
        log.debug("ValidationException is happened!");
        return "redirect:/";
    }
}

enum 사용하기 - 이슈의 카테고라이징 하기

  • 주어진 몇개만 제공하고, 카테고리(Label)값만 가지고 맞는지 아닌지만 식별해내면 됨 : 라벨은 어떠한 역할을 하지않음(따로 인스턴스를 여러개 만들지않음)
public enum Label {
    BUG(1), HELP(2), QUESTION(3);

    private long id;

    Label(long id) {
        this.id = id;
    }

    public static Label get(long id) {
        return Arrays.stream(Label.values()).filter(label -> label.id == id).findFirst().orElseThrow(EntityNotFoundException::new);
    }

    public static List<Label> getAll() {
        return Arrays.asList(Label.values());
    }

    public String getName() {
        return name();
    }
}

배포 - 웹서버 + 웹어플리케이션서버

  • 환경설정
    • nginx 설치
    • jdk 설치
    • jdk 심볼릭 링크, 환경변수 설정
  • 빌드(gradle - gradlew 사용) 후 run : ./build/libs
    • 기본으로 빌드과정에서 test를 함 : ./gradlew build
    • test 과정 필요없이 빌드하기 : ./gradlew build -x test
  • 아키텍쳐
    • 웹서버 + 웹어플리케이션서버 + 데이터베이스서버 : 추가적으로 웹서버 앞 단에 로드밸런서를 두어서 처리할 수도 있음
    • 웹서버 역할 : WAS로 요청 위임, 몸빵 역할 - WAS가 여러 이유에 의해서 요청 위임을 받을 수 없을 때 특정 페이지(static)를 띄울 수 있도록
  • nginx 프록시 및 점검페이지(WAS 요청위임 불가 상태 시 띄울 페이지) 설정
    • 프록시 : nginx(웹서버)가 클라이언트로부터 요청을 받고, 요청 처리는 tomcat(웹어플리케이션서버)이 하도록 위임(proxy_pass)
    • 점검페이지 : WAS 에러 시, 배포 시 요청을 위임하지못할 때 클라이언트에 응답해줄 페이지 설정, nginx static 파일 디렉토리(/usr/share/nginx/html)
    • 해당 conf 파일을 sites-available에 서버 설정 파일을 만들어두고, 만든 파일을 sites-enable에 심볼릭링크 생성해두면 됨 : /etc/nginx/nginx.conf 가 상위파일이고, 상위 파일에서 sites-enable에 생성한 설정 링크 파일을 읽음(관리와 실제 설정 변경 환경을 따로 나눠둠)
upstream tomcat {
    ip_hash;
    server 127.0.0.1:8080;
}

server {
    listen 80;

    server_name localhost;
    charset utf-8;    

    error_page 404 502 @error;
    location @error {
        root /usr/share/nginx/html;
        try_files /index.html /index.html;
    }

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;

        proxy_pass http://tomcat;
        proxy_redirect off;

    }
}
  • 쉘스크립트를 이용한 배포 자동화
    1. 자동 배포를 위한 스크립트 작성
    2. 정기적으로 스크립트 실행을 위해서 crontab 설정
    3. 에러 발생한다면 스크립트 실행 권한 주기 : $chmod a+x deploy.sh
/* deploy.sh */
#! /bin/zsh
export JAVA_HOME="$HOME/java"
export JAVA_OPTS="$JAVA_OPTS -Djava.security.egd=file:/dev/./urandom"

REPO=$HOME/java-ims
TARGET=$REPO/build/libs

cd $REPO
git fetch
origin=$(git rev-parse imjinbro)
remote=$(git rev-parse origin/imjinbro)

if [[ $origin == $remote ]]; then
   exit 0
fi

git merge origin imjinbro

CURRENT_PID=$(pgrep -f java-ims)

if [ -z $CURRENT_PID]; then
else
    kill -2 $CURRENT_PID
    sleep 5
fi

#build gradle
./gradlew build -x test

if [ ! -d $TARGET ]
then
    exit 1
fi

cd $TARGET
java -jar java-ims-1.0.0.jar > /dev/null 2>&1

/* crontab */
*/5 * * * * ubuntu /home/ubuntu/java-ims/deploy.sh > /dev/null 2>&1

OOP

  • 도메인 : 큰 의미로는 개발해야할 기능을 뜻하고, 작은 의미로는 구현하려는 기능의 핵심 로직을 가진 객체(핵심 역할을 맡음)
    • 도메인 객체에서 처리하도록 해야지 도메인이 가진 상태값을 억지로 끌어와서 외부에서 처리하고 외부에서 도메인의 상태를 변경하려고 해서는 안됨 : 요청을 통해 위임만 하면 됨

AOP

AOP란

  • 핵심 로직과 부가기능 분리 : 핵심 로직과 부가기능을 따로 분리해서 응집 시켜버림 - 부가기능에서의 코드 변경이 핵심 로직에는 아무런 영향을 미치지않도록 하는 것
    • 코드의 디펜던시를 아예 끊어버림 : 디펜던시가 있다면 만약 트랜잭션 상에서의 설정이 변경되었을 때 핵심 로직 처리에도 어떤 영향이 미칠지 모름 -> 사용한다는 것만 알고, 변경되어도 핵심 로직 코드는 절대 변경되지않도록 해야하므로 코드 상에서의 디펜던시를 아예 없앰(개발자 좋아요)
  • 흩어져있던 부가기능의 모듈화 : 같은 기능을 하지만 중복되어있던 부가기능들을 한 곳으로 모아버림
  • 사용한다는 것만 표시할 뿐, 핵심 로직 코드 상에서는 부가기능의 어떠한 코드도 없어야함
    • 표시 방법 : 어노테이션, xml
/* issueService.class */
@Transactional(rollbackFor = CannotDeleteException.class)
public Issue delete(@LoginUser User user) throws CannotDeleteException {

}

AOP 적용 방법

디자인 패턴 사용

  • 핵심 로직에는 없으나 부가기능을 가진 객체에 핵심 로직 객체를 전달하기위해서는 DI 흔적이 있음(사용한다는 선언이 있음 - 아예 없애야함)
  • 또한 부가기능을 적용하려는 타겟 객체마다 타겟의 타입 인터페이스를 구현하면서 부가기능을 가진 클래스를 정의해야함

프록시 생성

  • JDK Proxy 팩토리 메소드 사용 : 디자인 패턴에 비해 유연해짐, 클래스를 안만들어도 됨, 스프링 프록시 팩토리빈도 팩토리 메소드 사용 - 어드바이스, 포인트컷, 타겟을 완전 프록시에서 분리해내어서 어드바이스 - 포인트컷을 각각 하나만 생성해두고 재활용 할 수 있게 됨

바이트 코드 조작

  • CGLib 사용 : Code Generatoration Library(프록시 아닌 방식) - 타겟의 바이트 코드 조작, 컨테이너에 프록시 생성을 위한 빈을 등록해두지않아도 됨, private 메소드 등에도 부가기능 적용 가능, 사용하기위해서는 별도의 바이트코드 컴파일러를 사용하거나 클래스 로더를 사용해야함(번거로움), 그럼에도 불구하고 스프링부트 2.x 부터 바이트 코드 조작 방식을 사용하는 이유가 있을까?

AOP 하기위해 필요한 개념

  • 어드바이스(Advise) : 순수 부가기능만 가진 객체
  • 포인트컷(Pointcut) : 타겟으로 설정한 객체가 부가기능 적용 대상인지 기준을 가진 객체 - class, method 기준을 모두 정의할 수 있음
  • 애스펙트(Aspect) : 어드바이스와 포인트컷을 가진 객체(어떤 부가기능을 어느 객체에 적용할 것인지를 가진 객체 - Advisor), 부가기능의 모듈화 결과물
  • 타겟 : 부가기능이 적용되려는 객체 - 부가기능의 대상은 method
  • 프록시 팩토리빈 : 타겟과 어드바이스, 포인트컷을 모두 DI받고 타겟 객체의 타입(인터페이스) 인스턴스를 생성한 후 기존에 등록되어있던 스프링빈과 바꿔치기 됨
  • 프록시 : 부가기능이 적용된 타겟 타입의 객체(결과)
@Component
public class TransactionProxyFactoryBean implements FactoryBean<Object> {
    ...
}
  • 스프링은 조금 더 추상화 : 스프링은 위와 같이 팩토리빈을 등록해두지않고, 포인트컷과 어드바이스를 합친 어드바이저와 프록시 자동생성기를 빈으로 등록해두면 스프링빈 초기화때 빈 중 기준(포인트컷)에 맞는 객체를 프록시 생성하고 빈 바꿔치기 등록을 함
    • 스프링빈 컨테이너 또한 확장에는 열려있음 : 빈 후처리기(중 하나인 DefaultAdvisorAutoProxyCreator)를 지원 - 스프링빈으로 생성되는 빈에 대한 생성 후처리를 할 수 있음
@Configuration
public class AutoPrxoyConfig {
    
    @Bean(name = "transactionAdvisor")
    public Advisor createTransactionAdvisor() {
        new DefaultPointcutAdvisor(createNameMatchClassMethodPoint(), new TransactionAdvice());
    }
    
    private NameMatchClassMethodPointcut createNameMatchClassMethodPoint() {
        ...
    }
    
    @Bean(name = "autoProxyCreator")
    public DefaultAdvisorAutoProxyCreator createAutoProxyCreator() {
        return new DefaultAdvisorAutoProxyCreator();    
    }
}