코드 분석 및 리뷰

오의석·2023년 2월 22일
0

프로젝트 리뷰

목록 보기
1/1
post-thumbnail

SSAFY 공통 프로젝트 백엔드 코드 리뷰

환경: gradle, mysql, querydsl..

0. Spring 폴더 구조 및 약간의 컨벤션 and so on

(0) 패키지 구조 : com패키지명 -> 메인url패키지명 -> 프로젝트패키지명 -> 기능별로 패키지 구성 -> { controller, domain(Entity), dto{request,response}, exception, repository, service }

(1) Configuration클래스 내에서 다른 클래스를 사용할 때에는 Bean으로 설정한다.(외부 라이브러리 사용시에도) Bean 매소드 이름은 해당 클래스 이름에서 앞에만 소문자.

(2) 그 외에는 Component, Service, Repository와 같은 어노테이션으로 해당 클래스를 스캔할 수 있게 설정한다.

그리고 RequiredArgsConstructor와 private final로 해당 클래스를 주입한다.
(RequiredArgsConstructor는 final이 붙거나 @NotNull 이 붙은 필드의 생성자를 자동 생성해주는 롬복 어노테이션)
(쓰는 이유 : 런타임 의존성 가능. 명시적인 의존성. 불변. 테스트 편함.)

(3) Builder를 생활화하자.

(4) 기능 하나당 하나의 매서드 생활화!!(이건 지키기 어려웠음)

1. build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'			//jpa세팅. 자바에서 관계형 데이터베이스 사용하기 위함
	implementation 'org.springframework.boot:spring-boot-starter-security'			//사용안함. 프로젝트가 빨리 끝날 경우 바꿀 예정이었음
	implementation 'org.springframework.boot:spring-boot-starter-validation'		//유효성 검증. request 부분에서 NotBlank과 같은 유효성 검증 가능
	implementation 'org.springframework.boot:spring-boot-starter-web'				//swagger-ui를 사용하려고 세팅함
	implementation 'org.springframework.boot:spring-boot-starter-websocket'			//webrtc 부분에서 웹소켓으로 설정을 해줘야 하는 부분이 존재했음

	//kurento 설정
	implementation 'org.webjars.bower:kurento-utils:6.7.0'							//webrtc 부분
	implementation 'org.kurento:kurento-client:6.18.0'								//webrtc 부분

	//queryDsl 설정
	implementation 'com.querydsl:querydsl-core'										//qeuryDSL 세팅
	implementation 'com.querydsl:querydsl-jpa'										//qeuryDSL 세팅
	testImplementation 'org.projectlombok:lombok:1.18.22'							
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // querydsl JPAAnnotationProcessor 사용 지정
	annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError(javax.annotation.Entity) 발생 대응
	annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError (javax.annotation.Generated) 발생 대응

	//swagger 설정
	implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'	//swagger 세팅
	implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'		//swagger 세팅


	compileOnly 'org.projectlombok:lombok'													//롬복을 사용하기 위함
	developmentOnly 'org.springframework.boot:spring-boot-devtools'							//'자동으로 어플리케이션을 재시작'과 같은 개발 편의 모듈 제공
	runtimeOnly 'com.h2database:h2'															//h2database 사용가능(테스트용도 였던 걸로 기억)
	runtimeOnly 'com.mysql:mysql-connector-j'												//MYSQL 사용
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

implementation : 항상 적용.
debugImplementation : 디버그 빌드 시에만 적용.
releaseImplementation : 릴리즈 빌드 시에만 적용.
testImplementation : 테스트 코드를 수행할 때만 적용
compileOnly : 컴파일 시점에만 사용
runtimeOnly : 실행 시점에 필요한 라이브러리

2. application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver										#MYSQL 선택
    username: ID
    password: PASSWORD
    url: jdbc:mysql://mysqlurl/schema?serverTimezone=UTC&characterEncoding=UTF-8
	
  # swagger
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
	  
  jpa:
    database: mysql   #MYSQL 선택
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect 					# JPA 데이터베이스 플랫폼을 지정합니다.
    hibernate:
      ddl-auto: none											 					# 테이블 생성시에는 create, 변경된것만 반영 update, 엔티티와 테이블 매핑만 확인 validate, 
    properties:
      hibernate:
        show_sql: true																# sql문이 로그로 보임
        format_sql: true															# sql 로그의 가독성을 높여준다

  ## page 인덱스를 1부터 시작하게 만듬
  data:
    web:
      pageable:
        one-indexed-parameters: true
  # sql:
  #   init:
  #     mode: ALWAYS																# 항상동작
  #     continue-on-error: true														# 에러발생해도 동작
  #     schema-locations: classpath*:JUSTUDY_INIT_DATABASE.sql						# 테이블 관련 sql파일
  #     data-locations: classpath*:JUSTUDY_INIT_DATABASE.sql						# 테이블 데이터 관련 sql파일

logging:
  level:
    org.hibernate.sql: debug														#sql 로그 보기
    org.hibernate.type: trace														#sql 입력 데이터 로그 보기

vue:
  loginUrl: "localhost:8081/login"

3. 어노테이션 총 정리

@Slf4j, @Log4j2
@RequiredArgsConstructor # (access = AccessLevel.PROTECTED)
@Override
@Builder

@Configuration
@PersistenceContext # EntityManager를 빈으로 주입할 때 사용하는 어노테이션
@Bean

@RestControllerAdvice # @ControllerAdvice와 @ResponseBody를 합쳐놓은 어노테이션
@ControllerAdvice # 발생한 예외를 한 곳에서 관리하고 처리할 수 있게 도와주는 어노테이션
@ExceptionHandler
@ResponseStatus

@Component
@Service
@Repository

@Transactional # (readOnly = true) 타입도 줄 수 있다.
@PropertySource # ("classpath:file.properties")와 같이 사용하여, 해당 파일의 설정값을 읽을 수 있다.
@Value # ("${file.dir}")와 같이 적혀있으면, PropertySource에서 세팅한 파일에 file.dir에 저장된 값을 불러온다.

@Table
@Entity
@Id
@GeneratedValue
@Column
@OneToMany ,@ManyToMany, @OneToOne..등등 #(fetch = FetchType.LAZY) 써주기. @JoinColumn으로 컬럼명 지어주기

@SuppressWarnings # ("unchecked")
@RequestMapping
@GetMapping,@PostMapping,@DeleteMapping..등등

@RequestBody
@NotBlank
@Validated

@JsonCreator
@JsonProperty

4. config 패키지

(0)StringToEnumConverterFactory : (4)번의 WebConfig에서 컨버터부분을 추가하기 위해서 만드는 클래스이다.(그래서 ConverterFactory 인터페이스를 상속받는다.)

String Type을 Custom한 ENUM 관련 클래스로 변환하는 것을 담고 있다.

public interface ConverterFactory<S, R> {

	/**
	 * 파라미터 S는 변환전 타입
	 * 파라미터 T는 변환할 클래스의 범위를 정의하는 기본 타입
	 * 파라미터 R는 변환할 클래스
	 */
	<T extends R> Converter<S, T> getConverter(Class<T> targetType);

}

TIP 변수의 타입을 찾을 때는 보통 'instanceof '를 사용하지만 클래스의 타입을 찾을 때는 'class.isAssignableFrom'를 사용한다.

(1) QueryDslConfig 설정 (@Configuration설정 필요)

EntityManager를 @PersistenceContext로 감싼다. (동시성 문제를 해결하기 위함)
JPAQueryFactory를 빈으로 만들어 해당 EntityManager를 받아서 사용하게 설정한다.
(주입되는 건 스프링의 프록시 객체. 프록시 객체는 싱글톤으로 관리되지만, 해당 프록시 객체 내부에서 현재 트랜잭션에 맞게 적절한 영속성 컨텍스트에 연결시켜준다.)

(2) Swagger2Config 설정 (@Configuration,@EnableSwagger2 설정 필요)

Docket 함수 :
groupName : Docket이 여러개면 이부분의 이름을 다르게 해야한다.
select : ApiSelectBuilder 생성
apis : api 스펙이 작성되어 있는 패키지 지정(여러개면: 기능별로 패키지 넣기, 하나면: 최상위 패키지이름)
paths : 해당 url만 찾아서 할당
build : 빌드
useDefaultResponseMessages : swagger에서 응답코드 제공(false로 한 이유: 우리가 만든 에러 코드를 쓰기위함)
ignoredParameterTypes : 해당 클래스는 예외로 넣지 않는다.
apiInfo : ApiInfo 함수에서 리턴한 값을 넣어주면 된다. api설명서 첨부

ApiInfo 함수 : 해당 api 설명을 적을 수 있다.(title,버전,내용..등) 해당 내용은 Docket에 연결한다.

(3) Interceptor 폴더

  • AdminCheckInterceptor(HandlerInterceptor 인터페이스 상속 받음) : 관리자 계정 확인 인터셉터 {return 값이 true면 성공,false는 실패}
  • LoginCheckInterceptor(HandlerInterceptor 인터페이스 상속 받음) : 로그인 확인하는 인터셉터 {return 값이 true면 성공,false는 실패}
  • NotLogin : CustomException(5번 보면 됨) 받음. 로그인 되지 않았을 때, 나타나는 에러 메세지 할당

(4) WebConfig 설정

  • WebMvcConfigurer 인터페이스 상속 받음 : addCorsMappings,addFormatters,addInterceptors를 사용하기 위함(Override를 위에 추가해야 동작함)
    WebMvcConfigurer 클래스는 모두 default 매소드로 구성되어 있다.(하위호환성 때문이다. 인터페이스에 새로운 매소드를 만들어야하는 상황에 오류를 최소화하고 호환성을 높이기 위해서다.)

    addCorsMappings : addMapping은 cors를 적용할 url패턴, allowedMethods는 허용할 http method(GET,POST..),
    	allowCredentials는 쿠키 허용 여부, maxAge은 원하는 시간만큼 pre-flight 리퀘스트 캐싱 가능 
    
    addFormatters	: Converter와 Formatter를 등록해줄 수 있다. 현재는 Converter만 등록했다.
    
    addInterceptors : (3)폴더에 구성한 설정 클래스들을 Bean으로 할당한 후,InterceptorRegistry에 추가 해준다.

5. exception 패키지

(0) ExceptionController : @RestControllerAdvice 어노테이션으로 에러에 관해 전역적으로 설정해준다.

@ExceptionHandler설정으로 해당 클래스가 발생시 다음 매서드가 동작. (여기에 예외 발생시 우리가 만든 클래스를 지정해준다.)

(1) ErrorResponse : ExceptionController에서 출력할 response데이터 타입

(2) CutomException : RuntimeException 인터페이스를 상속 받아서 해당 에러를 출력할 수 있다.

6. common 패키지의 ENUM_UTIL 부분

구조 : EnumMapper { EnumModel, EnumValue } => EnumMapper => EnumConfig
//앞으로 만들 Enum은 EnumModel을 상속 받으면 됨. 다만 EnumConfig에서 Mapper에 넣어야함.
(Mapper put에는 대소문자 구분x. 이유는 StringToEnumConverterFactory - WebConfig 에서 toLowerCase로 설정함)
=> EnumController로 Enum 테스트 가능
설명 : EnumValue에서 key, value 변수 설정.
EnumModel(interface)에서 key, value getter 설정.
Custom한 Enum에는 EnumModel을 상속받기. 그리고 key는 name(), value는 입력받은 값을 넣으면 된다.
EnumConfig에 EnumMapper를 Bean으로 설정. Bean내에서 EnumMapper에는 put함수에서 해당 Enum이름과 value값을 넣으면 됨.

7. file 패키지(url로 이미지만 불러오는 방식) == 저장부분이 실패한 것으로 알고 있음... 테스트 필요

(0) service 패키지에 FileStore라는 클래스 만듦. 해당 클래스는 파일 다운로드 및 경로 불러오는 매서드로 구성되어 있다.

storeFile 매서드 : MultipartFile을 파라미터로 받음. multipartFile.transferTo로 저장한다.

(1) controller 패키지의 showImage라는 매서드 : GetMapping으로 되어 있음. Resource를 리턴.

new UrlResource("file:{절대경로}");의 형태로 return. 
[UrlResource는 java.net.URL를 감싸고 일반적으로 URL로 접근할 수 있는 파일, HTTP 대상, FTP 대상 등과 같은 객체에 접근하는데 사용할 수 있다.]
[ClassPathResource도 사용가능. 클래스 패스에서 파일 접근 가능. UrlResource는 외부 http url도 연결 가능]

8. 외부 restcontoller 접근

(0) member패키지의 service 패키지의 MemberService에 저장되어 있음.

validateMatterMost 매서드 :
RestTemplate 이용. exchange 매서드 이용.(파라미터로 RequestEntity와 ResponseEntity로 받을 클래스 넣어줌)
RequestEntity에는
해당되는 method타입에 uri 넣기(아래타입의 uri).
header에 host값, origin값 넣기 (host,origin은 이유 아직 모르겠음)
body에 입력할 값 보내기

URI는 UriComponentsBuilder를 이용해서 만듦. 
	fromUriString는 uri 넣기 (".com"까지 넣기)
	path에는  ".com"이후 경로
	encode 설정
	build + toUri로 변환

(1) 실제 실무자 분이 추천해준 방법

OkHttp 이용하기
특징 : HTTP/2 지원, 연결 풀링은 요청 대기 시간을 줄여준다. GZIP은 다운로드 크기를 줄인다. 응답 캐싱은 반복 요청에 대해 네트워크를 완전히 피한다.

//의존성 추가
implementation 'com.squareup.okhttp3:okhttp:4.9.3'

- 동기식 :

private final OkHttpClient client;
public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
...
		RequestBody body = RequestBody.create(json, JSON);
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        try (Response response = client.newCall(request).execute()) {
            return response.body().toString();
        }

- 비동기식 :

private final OkHttpClient client;
public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
...
		RequestBody body = RequestBody.create(json, JSON);
		Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        //비동기 처리 (enqueue 사용)
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                fail();
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                System.out.println("Response Body is " + response.body().string());
            }
        });

- 장점 : Application Interceptors, Network Interceptors, 기본설정값

9. Pageable (Controller에서 request 파라미터에 설정해주면 됨)

Controller에 세팅해줄때
@GetMapping
    public Page<ExpenseResponseDto> getAllExpenses(Pageable pageable) {
        return expenseService.getAllExpenses(pageable);
    }
파라미터에 들어갈 내용
"pageable": {
        "sort": {
            "empty": false,
            "sorted": true,
            "unsorted": false
        },
        "offset": 0,
        "pageSize": 2,
        "pageNumber": 0,
        "paged": true,
        "unpaged": false
    },

10. queryDSL 구조.

(0) Repository 패키지:

	Repository	: "JpaRepository<Entity타입, 시퀀스 타입>인터페이스와 CustomRepository"를 extends로 상속받는다.
	CustomRepository (인터페이스) : 내가 만들 함수명을 선언한다.
			{ 하나일 경우 Optional, 여러개일 경우 List로 받자 }
			[명명 규칙 : select는 Find, where절은 by사용(조건 방식에 따라 And ,Or 추가), 검색은 By컬럼명Like/By컬럼명NotLike, 
				시작 검색은 By컬럼명StartingWith, 끝검색은 By컬럼명EndingWith, null에 따른 검색은 By컬럼명IsNiull/By컬럼명IsNotNukk],
				특정 컬럼 boolean검색은 By컬럼명True/By컬럼명False, 특정 컬럼 시간 검색은 By컬럼명Before/By컬럼명After,
				숫자 비교는 By컬럼명LessThan/By컬럼명GreaterThan(같은경우가 있을 경우 Equal붙이기), 두 숫자 사이 검색은 By컬럼명Between,
				순서 사용시 OrderBy검색할엔티티Asc/OrderBy검색할엔티티Desc, count는 countBy(조건이 있으면 뒤에 컬럼명 붙인다.) ]
	RepositoryImpl	(CustomRepository를 상속받음) : JPAQueryFactory와 Q클래스엔티티 주입
			Optional은 	Optional.ofNullable를 사용. 
			(수정,삭제,생성은 service에서 한다.)
			

[TIP join (좀 더 공부 필요) ]

//기본 조인 //join(...).on 일경우 일반 교집합 조인과 동일 
queryFactory.selectFrom(/*select 대상 entity*/)
.join([( 대상 entity ).(Join 대상 Entity)], [별칭])
.where ....
.fetch();

//조인 on //엔티티 데이터는 전체를 다 긁어 온다음 JE.column 이 data 면 데이터 조회, data가 같지 않으면 이 null 로 출력
queryFactory.select([main_entity(ME)],[join_entity(JE)])
	.from([ME])
    .leftJoin([ME.JE],[별칭]).on([JE.column].eq([data]))
    .fetch();
	
//연관관계가 없는 조인 on //연관관계가 없는 조인같은 경우는 부모.연관컬럼  으로 join 하지 않는다.
jpaQueryFactory.select(member, team)
.from(member)
.leftJoin(team)
.where(member.username.eq(team.name))
.fetch();

//fetchJoin 페치 조인 // parent -> children 연관관계 테이블을 조인하여 데이터를 넣을때사용
jpaQueryFactory
  .selectFrom(memberMst)
  .join(memberMst.orders, orderMst).fetchJoin()
  .where(memberMst.memberMstId.eq("MM20220212000003"))
  .fetch();

(1) Service : Repository와 Q클래스엔티티 주입

	생성 : Repository.save(Entity);
	읽기 : RepositoryImpl에서 만든 매서드 사용
	수정 : Repository.udpate(Entity).set(컬러명,값).where(조건).execute();
	삭제 : Repository.remove(entity).where(조건).execute();
	

(2) request (공부 필요)

@Data
상황에 따른 @NotBlank

(3) response (공부 필요)

@Data

(4) domain

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "member_activity")
@Entity
@Builder

11. schedule 관련 클래스 ( 서비스 패키지에 세팅하기)

(0) 어노테이션 세팅

@Slf4j
@EnableScheduling
@EnableAsync
@RequiredArgsConstructor
@Service

(1) 매서드에 주기 설정

@Scheduled(cron = "0 0 1 * * ?",zone = "Asia/Seoul")
//초 분 시 일 월 요일, 타임존

12. TEST. JUnit. 단위 테스트 관련(추가 예정)

profile
끊임없이 나아가는 사람이 되어볼게요.

0개의 댓글