SSAFY 공통 프로젝트 백엔드 코드 리뷰
환경: gradle, mysql, querydsl..
그리고 RequiredArgsConstructor와 private final로 해당 클래스를 주입한다.
(RequiredArgsConstructor는 final이 붙거나 @NotNull 이 붙은 필드의 생성자를 자동 생성해주는 롬복 어노테이션)
(쓰는 이유 : 런타임 의존성 가능. 명시적인 의존성. 불변. 테스트 편함.)
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 : 실행 시점에 필요한 라이브러리
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"
@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
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'를 사용한다.
EntityManager를 @PersistenceContext로 감싼다. (동시성 문제를 해결하기 위함)
JPAQueryFactory를 빈으로 만들어 해당 EntityManager를 받아서 사용하게 설정한다.
(주입되는 건 스프링의 프록시 객체. 프록시 객체는 싱글톤으로 관리되지만, 해당 프록시 객체 내부에서 현재 트랜잭션에 맞게 적절한 영속성 컨텍스트에 연결시켜준다.)
Docket 함수 :
groupName : Docket이 여러개면 이부분의 이름을 다르게 해야한다.
select : ApiSelectBuilder 생성
apis : api 스펙이 작성되어 있는 패키지 지정(여러개면: 기능별로 패키지 넣기, 하나면: 최상위 패키지이름)
paths : 해당 url만 찾아서 할당
build : 빌드
useDefaultResponseMessages : swagger에서 응답코드 제공(false로 한 이유: 우리가 만든 에러 코드를 쓰기위함)
ignoredParameterTypes : 해당 클래스는 예외로 넣지 않는다.
apiInfo : ApiInfo 함수에서 리턴한 값을 넣어주면 된다. api설명서 첨부
ApiInfo 함수 : 해당 api 설명을 적을 수 있다.(title,버전,내용..등) 해당 내용은 Docket에 연결한다.
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에 추가 해준다.
@ExceptionHandler설정으로 해당 클래스가 발생시 다음 매서드가 동작. (여기에 예외 발생시 우리가 만든 클래스를 지정해준다.)
구조 : 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값을 넣으면 됨.
storeFile 매서드 : MultipartFile을 파라미터로 받음. multipartFile.transferTo로 저장한다.
new UrlResource("file:{절대경로}");의 형태로 return.
[UrlResource는 java.net.URL를 감싸고 일반적으로 URL로 접근할 수 있는 파일, HTTP 대상, FTP 대상 등과 같은 객체에 접근하는데 사용할 수 있다.]
[ClassPathResource도 사용가능. 클래스 패스에서 파일 접근 가능. UrlResource는 외부 http url도 연결 가능]
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로 변환
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());
}
});
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
},
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에서 한다.)
//기본 조인 //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();
생성 : Repository.save(Entity);
읽기 : RepositoryImpl에서 만든 매서드 사용
수정 : Repository.udpate(Entity).set(컬러명,값).where(조건).execute();
삭제 : Repository.remove(entity).where(조건).execute();
@Data
상황에 따른 @NotBlank
@Data
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "member_activity")
@Entity
@Builder
@Slf4j
@EnableScheduling
@EnableAsync
@RequiredArgsConstructor
@Service
@Scheduled(cron = "0 0 1 * * ?",zone = "Asia/Seoul")
//초 분 시 일 월 요일, 타임존