현재 개발 중인 어드민 사이트는 다국어 지원이 되어야 한다. 

내가 맡은 부분은 서버 즉 백엔드 부분이므로, 설정한 언어에 맞게 Error Message를 클라이언트로 전달해야 했다.

 

기존 구 어드민 사이트에서는 .properties 파일에 Message들을 저장했지만 나는 DB에 저장하여 관리키로 함.

전략이라 할 것도 없지만.. 어쨌든 내가 가고자 하는 방향에 맞는 전략은 아래와 같다.

(내 전략의 방향이 맞는지, 그리고 이것을 구현한 방법이 맞는지 아직도 의문이다. 따라서 혹시라도 이것을 보시고 더 좋은 방법이나 틀린 부분이 있다고 느끼시는 분들이 있다면 Comment로 남겨주시면 제가 그것을 감사히 읽어보고 수정하도록 하겠습니다.)

 

- 에러 메시지는 DB에 저장

- 에러 메시지를 클라이언트로 전달할 때는 Caching되어진 데이터를 전달하고, 데이터는 locale과 messageKey 값으로 찾음 

- Exception은 @RestControllerAdvice로 지정한 class에서 처리

 

@RestContorllerAdvice로 설정된 전역 ExceptionHandler가 클라이언트에서 전달한 Accept-Language Header값에 맞는 Exception Message로 전달하는 역할을 담당한다. 각 메소드는 Exception에 대한 MessageKey를 가지고 있어 Caching되어진 Exception Data에서 MessageKey와 Locale 정보에 맞는 Exception Message를 불러와서 Client로 전달한다. 단, 어떤 Exception 은 구체화된 에러 메시지를 내려줄 필요성이 있기에 이런 경우에는 exception을 throw할 때 overriding한 CustomException의 messageKey를 set해서 전달한다. 아래 로직은 @ExceptionHandler가 Exception을 Client에 전달하기 전에 수행하는 작업에 대한 내용이다. 

@RestControllerAdvice
@Slf4j
public class AdminExceptionHandler {
    @ExceptionHandler(ForbiddenRequestException.class)
    public ResponseEntity<?> handleForbiddenRequestException(ForbiddenRequestException e, Locale locale) {
        log.error(ExceptionUtils.getExceptionMessage(e));
        String messageKey;
        if (e.getMsgKey() != null) {
        	messageKey = e.getMsgKey();
        } else {
            	messageKey = "forbidden.request";
        }
        return ApiMessage.builder().errorMessage(resource.getMessage(CacheKeys.builder().messageKey(messageKey).locale(locale.toString()).build())).status(e.getStatus()).build().toEntity();
    }
}

 

그리고 아래 부분은 DefaultLocale 값을 설정하고, Accept-Language 값을 Locale로 설정하기 위해 구현한 내용이다. 

나는 CookieLocaleResolver를 사용했다. 아래 설정을 마치면 @ExceptionHandler로 지정한 메소드로 전달한 Locale값이 Accept-Language 헤더값으로 잘 전달된다. 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Arrays;
import java.util.Locale;

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }


    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setDefaultLocale(Locale.KOREA);
        return localeResolver;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang");
        return lci;
    }
}

 

다국어 데이터를 Caching 하여 필요할 때 꺼내쓰려면 messageKey와 locale 두개의 값이 Caching 데이터의 Key가 되어야 했고 이를 위해 아래 CacheKeys라는 아래 class를 만들었다. 

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.io.Serializable;
import java.util.Objects;

@Getter
@Setter
@Builder
public class CacheKeys implements Serializable {
    private String messageKey;
    private String locale;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CacheKeys cacheKeys = (CacheKeys) o;
        return Objects.equals(messageKey, cacheKeys.messageKey) &&
                Objects.equals(locale, cacheKeys.locale);
    }

    @Override
    public int hashCode() {
        return Objects.hash(messageKey, locale);
    }
}

 

getMessage 메소드가 캐싱하는 대상 메소드이다.

보시다시피 key는 위에 정의한 CacheKey 클래스의 messageKey와 locale 값이다. 

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

import java.util.List;

@Slf4j
@Component
public class ErrorMessageComponent {

    @Autowired
    ErrorMsgI18nRepository errorMsgI18nRepository;

    @Cacheable(value = "errorMsgCache", key = "#cacheKeys")
    public String getMessage(CacheKeys cacheKeys) {
        log.debug("by getMessage method -> not by cache");
        return errorMsgI18nRepository.findByMsgKeyAndLocale(cacheKeys.getMessageKey(), cacheKeys.getLocale()).orElse("CACHE KEYS ERROR");
    }
}

 

class가 key로 되게 하기 위해서는 우선 내가 사용한 ehcache의 xml파일에 정의를 해주어야 한다.

key가 바로 CacheKeys라는 것을 명시해주고, value는 String임을 정의해준다.

<expiry>를 보면 서비스가 run하는 중에는 만료되지 않게 하기 위해 <none/>를 해준 것을 알 수 있다. 

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.ehcache.org/v3"
        xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
        xsi:schemaLocation="
            http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
            http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">

    <cache alias="errorMsgCache"> 
        <key-type>com.test.entity.CacheKeys</key-type> 
        <value-type>java.lang.String</value-type>
        <expiry>
            <none/>
        </expiry>

        <resources>
            <heap unit="entries">10</heap> 
            <offheap unit="MB">10</offheap>
        </resources>
    </cache>
</config>

 

 

  • Ehcache
    • Hibernate와 캐시 제공자 사이의 다리 역할
    • redis / memchached와 달리 별도 서버를 가지지 않으며, Spring 내부적으로 동작하여 캐싱함
    • 캐시하고 싶은 메소드에 @Cacheable(value = "xxx")를 붙여주면 된다. 
      • Second-Level Cache란?
        • 세션 단위의 1차 레벨 캐시라는 개념을 적용한게 Hibernate / 세션이 종료되면 First-Level Cache도 종료
        • Second-Level Cache는 SessionFactory 범위이므로 모든 세션에서 같은 SessionFactory를 공유한다. 
        • 엔티티가 Second-Level Cahing 활성화 상태가 되면? 
          • 인스턴스가 First-Level-Cache에 있는 경우엔 First-Level-Cache에서 반환된다.
          • First-Level-Cache에서 인스턴스를 찾을 수 없고 해당 인스턴스 상태가 Second-Level에 Cache된 경우 거기서 데이터를 가져온다. 
          • 그렇지 않으면 필요한 데이터가 데이터베이스에서 로드되고 Instance가 assemble되고 반환된다.

 

 

일정에 쫓기어 글을 완성도 있게 마무리 하고 싶었지만 우선 이정도로 마무리.. 

언제쯤이면 일도 척척 하고 포스팅도 여유롭게 할 수 있는 경지에 다다를 수 있을런지

얼른 끝내고 다시 돌아오겠습니다~

 

 

 

 

 

참고.

gs.saro.me/dev?tn=488

현재 개발하고 있는 프로젝트에서 사용하는 테이블들 간의 관계가 매우 복잡하다.

할 때 마다 까먹고, 정확히 사용하고 있는 건지 확신이 서지 않아 이번 기회에 다시 정리하기로 하였다.

 

먼저,

  • Cascade
    • @OneToMany, @ManyToOne 옵션 값 (parent-child 관계 일 때)
    • Entity의 상태가 변화했을 때 관계가 있는 Entity에도 상태 변화를 전파시키는 옵션 
    • Entity의 상태란?
      • Transient : 객체에 관해 아무것도 모르는 상태
      • persistent : 저장하여 JPA가 관리하는 상태로, 필요한 시점에 DB에 적용
      • detached : JPA가 관리하지 않는 상태 
      • removed : JPA가 관리는 하나, commit 시점에만 삭제
    • Type
      • type 명시하지 않을 시 모든 연관 관계 무시 
      • save-update 
        • save(), update() 하면 연관 관계 탐색해서 새로 생성된 비영속 인스턴스 저장, 준영속 인스턴스에서 발생한 변경사항 영속화
      • persist
        • 연관 관계에 있는 비영속 인스턴스를 영속화 하여, 해당 인스턴스를 호출 할때 연쇄 작용 일으킴
      • merge
        • 연관 관계에 있는 비영속 인스턴스를 영속 인스턴스로 병합
      • delete
        • delete(), remove()시 연관 관계에 있는 영속 인스턴스 삭제
      • remove
        • delete(), remove()시 연관 관계에 있는 영속 인스턴스 연쇄 삭제
      • lock
      • replicate
      • evict
      • refresh
      • all
        • 위 목록에 있는 모든 연쇄 옵션을 포함해 활성화 한다. 

 

 

수많은 @OneToMany의 향연 속에서 Parent가 삭제될 때 Child를 어떻게 할 것인가를 고민하다

Parent가 삭제되면 Child도 삭제하기로 결정. 그리고 구현방법을 찾아보다 아래 두가지 방법을 찾게 되었는데 

똑같아 보이는데 왜 다르지? 궁금증이 생김, 구글에 검색해서 내가 원하는 명확한 차이점을 찾았고(힘들었음..)

아래에 정리해 보았다. 

 

  • orphanRemoval=true  vs  CascadeType.REMOVE
    • Parent가 삭제 되었을 때 Child도 함께 삭제시키는 역할을 수행하는 점에선 동일
    • 다른점은,
      • 관계를 끊는 것에 대한 응답!
      • 만약 Parent의 Child 값을 null을 주었다고 가정
        • orphanRemoval=true는 관계가 끊어진 child를 자동으로 제거한다. 
        • CascadeType.REMOVE는 관계가 끊어진 child를 자동 제거하지 않는다.
          • 관계가 끊어졌다는 것을 제거로 보지 않기 때문에 

https://www.objectdb.com/java/jpa/persistence/delete#Orphan_Removal_

 

Using JPA to remove (delete) Java entity objects from the database

Existing entity objects can be deleted from the database either explicitly by invoking the removeremove(entity)EntityManager's methodRemove the entity instance.See JavaDoc Reference Page... method or implicitly as a result of a cascade operation. This page

www.objectdb.com

 

프론트 개발자분과 함께 작업하면서 백엔드 api서버를 로컬에 띄워 작업하는 데 한계를 느껴

아직 개발 중이지만 지금까지 만들어 둔 것만 EC2에 올려두기로 했다.

 

그런데 이미 EC2에 올려놓은 코드를 수정해야 할 작업이 계속 생기고,

그때마다 배포를 하는 것은 말도 안된다 생각하여 자동배포를 하기로 결심! 

 

동료의 도움과 아래 잘 정리된 포스팅으로 생각보다 빠르게 (반나절) 끝이 났다.

 

 

https://twofootdog.tistory.com/37

 

AWS CodeBuild로 빌드 후 S3에 빌드 결과파일 업로드

이번 글에서는 AWS CodeCommit에 있는 스프링부트 소스코드를 AWS CodeBuild를 통해서 빌드를 수행한 후 빌드된 결과파일(아티팩트(JAR))을 S3에 업로드하는 실습을 진행해볼 것이다. 이 글의 순서는 다음과 같다...

twofootdog.tistory.com

https://jojoldu.tistory.com/281

 

1) AWS로 배포하기 시리즈 - 1. Code Deploy 사용하기

AWS로 전체 시스템 구축해야 할 일이 생겨 AWS 배포 환경 시리즈를 시작합니다. 시리즈 과정은 CodeDeploy -> Code Pipeline -> ELB & Auto Scaling Group -> Beanstalk 으로 진행될 예정입니다. Code Pipeline vs..

jojoldu.tistory.com

일단 돌아가게는 만들었지만, 다시 정리할 필요를 느껴 짧게 내 방식대로 정리해보려 한다. 

 

 

 

 

 

AWS에서 배포하는 데 필요한 서비스

 

  • Code Commit
    • 깃허브와 같은 코드 저장소의 역할
  • Code Build
    • 빌드용 서비스, 규모가 있는 데에선 젠킨스/팀시티 등 이용
    • 소스코드 컴파일, 단위테스트, 배포 준비된 artifact file 생성
  • Code Deploy
    • 배포 서비스, 위 2개의 서비스는 대체재가 있으나 CodeDeploy는 대체재가 없다.
    • 오토 스케일링 그룹 배포, 블루 그린 배포, 롤링 배포, EC2 단독 배포의 기능을 지원한다. 

 

내가 이해한 Code Pipeline이 빌드 및 배포하는 과정 

 

Code Commit에 올린 내 소스가 업데이트(커밋)되면 파이프라인이 실행된다. (커밋 = 트리거)

소스 단계의 artifact가(빌드 준비가 완료된 파일)이 S3에 저장된다.  (빌드 준비가 완료된 파일)

CodeBuild는 이 artifact를 빌드한 다음, 빌드된 jar파일을 다시 S3에 저장하고, CodeDeploy에게 배포 명령을 내림

CodeDeploy는 CodeBuild가 빌드한 jar파일을 가져와서 애플리케이션(EC2)에 배포한다. 

 

 

 

 

자동배포 작업순서 

 

  • Code Commit에 내 스프링부트 소스 올려두기
  • AWS S3 버킷 생성하기
  • AWS CodeBuild 프로젝트 생성 
    • 소스공급자 - CodeCommit
    • 리포지토리 - CodeCommit 내 나의 프로젝트 
    • 브랜치 - master
    • 운영체제 - Amazon Linux 2
    • 아티팩트 - Amazon S3 (버킷 연결)
      • 패키징 : zip 
      • 캐시 : 빌드에 필요한 의존성들을 캐시하는 건데, 빌드할 때마다 새로 받는게 아니라 S3에 올려놓고 이미 있는 건 새로 받지 않도록 하는 것
  • 프로젝트 Root 디렉토리에 buildspec.yml 파일 생성
    • artifacts작성하면서 jar파일 어디에 저장하고 Deploy때 필요한 appspec.yml과 배포스크립트 파일 어디있는지 작성하기
  • commit & push 후 CodeBuild에서 빌드 시작 클릭해서 잘 되는지 테스트
  • AWS CodeDeploy용 IAM 역할 생성 (AWSCodeDeployRole)
  • AWS EC2 인스턴스에서 S3 접근을 위한 IAM 역할 생성 (AmazonEC2RoleforAWSCodeDeploy)해서 EC2에 역할주기
  • EC2 인스턴스에 CodeDeployAgent 설치
$ sudo yum update
$ sudo yum install ruby
$ sudo yum install wget

$ cd /home/ec2-user
$ wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
$ chmod +x ./install
 

최신 버전의 CodeDeploy 에이전트를 설치하려면 다음을 수행합니다.

$ sudo ./install auto

서비스가 실행 중인지 확인하려면 다음 명령을 실행합니다.

$ sudo service codedeploy-agent status

"error: No AWS CodeDeploy agent running"와 같은 메시지가 표시되면 서비스를 시작하고 다음 두 명령을 한 번에 하나씩 실행합니다.

$ sudo service codedeploy-agent start
$ sudo service codedeploy-agent status

 

출처 : https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/codedeploy-agent-operations-install-linux.html

 

  • AWS CodeDeploy 애플리케이션 생성
    • AWS CodeDeploy용 IAM 역할 붙여주기
    • 애플리케이션 배포 방법 선택 (현재위치, 블루/그린)
    • 연결할 ec2를 tag를 입력하여 알려주기
  • 프로젝트 Root 디렉토리에 appspec.yml 파일 생성
    • 어떤 파일을 어느 위치에 배포하고 어떤 스크립트 실행할 건지 관리한다.
  • 배포스크립트 start.sh 생성 (아래 참고)
  • commit & push 후 CodeDeploy에서 배포 생성해서 잘 되는지 테스트해보기

+ deploy할 때 에러가 발생할 때는 ? 

  • cd /var/log/aws/codedeploy-agent 로 가서 codedeploy-agent.log 파일을 확인해보자.

 

 

CI/CD 흐름

설정한 배포 프로세스

 

작성파일 

 

  • buildspec.yml
version: 0.2

phases:
  install:
    runtime-versions:
      java: corretto8
  build:
    commands:
      - echo starting build stage
      - mvn package
  post_build:
    commands:
      - pwd
artifacts:
  files:
    - target/*.jar
    - appspec.yml
    - scripts/**
  discard-paths: yes
 cache:
  paths:
    - '/root/.m2/**/*'

artifacts는 빌드된 빌드된 WAR/JAR를 보관할 위치를 나타낸다.

artifacts 파일에 있는 appspec.yml과 scripts/** 는 CodeBuild가 .zip으로 패키징 할 때 jar파일에 appspec.yml, start.sh를 포함시킨다. CodeDeploy는 S3에 있는 .zip파일 압축을 풀어서 appspec.yml파일을 참고해서 배포한다. 

cache 경로에 있는 파일들이 S3 캐시파일로 등록된다.

 

 

  • appspec.yml
version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/target/
permissions:
  - object : /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

hooks:
  AfterInstall:
    - location: start.sh
      timeout: 60
      runas: ec2-user

 

  • 배포스크립트 (.sh)
#!/bin/bash
BUILD_JAR=$(ls /home/ec2-user/target/*.jar)
JAR_NAME=$(basename $BUILD_JAR)
echo "> build 파일명: $JAR_NAME" >> /home/ec2-user/deploy.log
echo "> debugging : $BUILD_JAR"    >> /home/ec2-user/deploy.log

echo "> build 파일 복사" >> /home/ec2-user/deploy.log
DEPLOY_PATH=/home/ec2-user/target/
# cp $BUILD_JAR $DEPLOY_PATH

echo "> 현재 실행중인 애플리케이션 pid 확인" >> /home/ec2-user/deploy.log
CURRENT_PID=$(pgrep -f $JAR_NAME)
echo "> debugging : $JAR_NAME"    >> /home/ec2-user/deploy.log

if [ -z $CURRENT_PID ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." >> /home/ec2-user/deploy.log
else
  echo "> kill -15 $CURRENT_PID"
  kill -15 $CURRENT_PID
  sleep 5
fi

DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME
echo "> DEPLOY_PATH, JAR_NAME : $DEPLOY_PATH$JAR_NAME"    >> /home/ec2-user/deploy.log

echo "> DEPLOY_JAR 배포"    >> /home/ec2-user/deploy.log
nohup java -Xms1024m -Xmx2048m -XX:NewRatio=2 -server -Dspringi.profiles.active=product -jar $DEPLOY_JAR >> /home/ec2-user/deploy.log 2>/home/ec2-user/deploy_err.log &

 

Code Pipeline으로 배포환경을 구축할 때 발생했었던 에러

  • 평소 Maven Wrapper을 사용해서 ./mvnw로 빌드했는데, 이게 안먹는거 아닌가;
    • ./mvnw은 안되는 걸로... (아무리 검색해봐도 관련 자료가 없다 ㅠㅠ)
    • mvn package로 하자..
  • '기본 manifest 속성이 없습니다' 오류
    • Code Pipeline에서는 모든 과정이 오류 없이 Success가 떴는데, 서버가 실행되지 않았다.
    • 문제는  jar파일이 처음부터 잘못 만들어진 것인데
    • 원인은 'spring-boot-maven-plugin' 플러그인 추가를 하지 않았던 것
      • spring-boot-maven-plugin
        • 실행 가능한 jar 파일을 만들어 준다. 
          • 실행가능한 jar파일
            • 컴파일된 클래스 뿐만 아니라 코드 실행에 필요한 모든 jar dependency들을 포함하여 압축한다. 
        • 의존하고 있는 라이브러리 들을 추가해준다. 
        • BOOT-INF에 컴파일된 class 파일을 저장하는데, JarLauncher는 이 BOOT-INF 구조에 있는 class파일과 .jar파일을 로딩한다. 

 

 

public void atomicInteger1() {
    AtomicInteger atomic = new AtomicInteger();
    System.out.println("value : " + atomic.get()); // 0

    AtomicInteger atomic2 = new AtomicInteger(10);
    System.out.println("value : " + atomic2.get()); // 10
}

Java에는 Concurrent 문제를 해결하는 데 3가지 방법이 있다.

  1. volatile 이용 
  2. Atomic 클래스 이용
  3. synchronized 이용
  • volatile
    • 왜 필요한가?
      • 각각의 컴퓨터에서 어플리케이션을 실행하면, 각 쓰레드는 main memory에서 읽어온 변수 값을 각 CPU의 Cache로 복사하여 읽는다.  
      • 각 쓰레드가 같은 java 변수값을 가져왔다고 쳐도, 쓰레드가 CPU Cache에 저장된 값을 변경하고 메모리에 제때 쓰지 않는다면 각 쓰레드에서 사용하는 변수값이 일치하지 않을 수가 있다.
        • 한 쓰레드는 해당 변수를 카운트하고, 다른 쓰레드는 해당 변수를 ++ 한다고 가정하면, 카운트하는 쓰레드는 CPU캐시에만 업데이트 된 값을 가져올 수 없다는 것. 
      • 변수의 가시성 문제를해결할수있다.
        • 가시성 문제
          • CPU 메모리와 Main 메모리 중에서 어디서 가져온 값인지 알 수 없다.
          • 여러 스레드에서 변수에 대한 변경 사항의 가시성을 보장 
    • volatile변수는 컴퓨터의 메인 메모리부터 read하고, 메인 메모리에 직접 write 한다. 
    • 어떻게 쓰는가?
private volatile int count = 0;
  • atomic
    • CAS(Compare and Swap) 방식 
      • 변수의 값을 변경하기 전에 기존의 값이 내가 예상하던 값과 같을 때만 새로운 값으로 할당
public void atomicInteger2() {
    AtomicInteger atomic = new AtomicInteger();
    System.out.println("value : " + atomic.get()); // 0

    atomic.set(100);
    System.out.println("value : " + atomic.get()); // 100
}
  • atomic vs volatile
    • volatile 키워드는 오직 한개의 쓰레드에서 쓰기 작업을 하고, 다른 쓰레드는 읽기작업만을 할 때 사용
    • AtomicBoolean, AtomicInteger는 여러 쓰레드에서 읽기/쓰기 작업을 병행할 수 있다.
  • synchronized

출처

https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html

https://rightnowdo.tistory.com/entry/JAVA-concurrent-programming-Visibility%EA%B0%80%EC%8B%9C%EC%84%B1

http://tutorials.jenkov.com/java-concurrency/volatile.html

https://readystory.tistory.com/53

https://codechacha.com/ko/java-atomic-integer/

 

 

개인적인 일로 2020년이 되어서 도통 개발 공부할 시간이 없었다.

아니 남는 시간에 하려다 보니 공부할 시간이 없었던 거라고 볼 수 있다.

 

오늘 사수님이 공유해주신 포스팅을 보고 내가 진짜 개발자가 맞나라는 생각이 들었고,

나도 같은 자바 개발자로서 해당 포스팅 주인공 분과 같이 개발 책을 열심히 공부하기로 마음 먹었다. 

 

친절하시게도, 공부하신 책들을 다 소개해주셔서 나는 내 수준에 맞는 책들만 순서대로 보면 될 것 같다. 

 

개발자가 직업이라면 공부는 필수다.

 

조금씩 천천히 꾸준하게!

이 중에서도 제일 중요한 건 꾸준함이라 생각한다.

 

그래서 나의 남은 2020년의 개발 포부를 책 리스트업과 함께 해보려 한다.

조금 정리가 필요하기에 계속 업데이트 하겠다.

 

-매일 아침 최소 30분, 최대 1시간 개발 공부를 한다. 

 

 

 

-자바의 정석을 최소 2번 읽는다. 

- Spring MVC을 hotire님이 정리하신 spring-core를 보고 이해한다. 

 

-모던자바인액션 (70% 완료)

-자바 ORM 표준 JPA 프로그래밍 (50% 완료)

-개발자가 반드시 알아야 할 자바 성능 튜닝 이야기 (60%완료)

- 운영체제 (-)

https://github.com/hotire/spring-core

 

+ Recent posts