알고리즘
같은 숫자는 싫어
로직
1.
입력받은 배열의 0번 element를 추가.
a.
2번 로직에서 비어있는 리스트(스택)와 비교하지 않도록 하기 위함.
2.
마지막 숫자와 다르면 추가.
a.
같은 숫자를 무시하고 다른 숫자를 추가.
스택 미사용
코드
public class Solution {
public int[] solution(int[] arr) {
List<Integer> answerList = new ArrayList<>();
// 로직 (1) : 입력받은 배열(arr)의 0번 element를 추가.
answerList.add(arr[0]);
// 로직 (2) 반복
// 0번 element는 이미 추가되어있으므로 1번부터 시작.
// arr의 element 개수만큼 반복.
for (int i = 1; i < arr.length; i++) {
// 로직 (2) : 마지막 숫자와 다르면 추가.
// 마지막 인덱스 = answerList.size() - 1
if (answerList.get(answerList.size() - 1) != arr[i]) answerList.add(arr[i]);
}
// List -> Array 변환.
int[] answer = new int[answerList.size()];
for (int i = 0; i < answerList.size(); i++) {
answer[i] = answerList.get(i);
}
return answer;
}
}
Java
복사
스택 사용
코드
public class Solution {
public int[] solution(int[] arr) {
Stack<Integer> stack = new Stack<>();
// 로직 (1) : 입력받은 배열(arr)의 0번 element를 추가.
stack.push(arr[0]);
// 로직 (2) 반복
// 0번 element는 이미 추가되어있으므로 1번부터 시작.
// arr의 element 개수만큼 반복.
for (int i = 1; i < arr.length; i++) {
// 로직 (2) : 마지막 숫자와 다르면 추가.
// 마지막 element = stack.peek() [마지막 element를 반환, 제거 X]
if (stack.peek() != arr[i]) stack.push(arr[i])
}
// List -> Array 변환.
int[] answer = new int[stack.size()];
for (int i = 0; i < stack.size(); i++) {
answer[i] = stack.get(i);
}
return answer;
}
}
Java
복사
스프링 부트MVC - 게시판
save() - DuplicatedEntity
Spring Data JPA의 save() 동작
SimleJpaRepository
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
Java
복사
1.
입력받은 엔티티가 새로운 엔티티인지 확인. (entityInformation.isNew(entity))
2.
새로운 엔티티라면 저장. (em.persist(entity))
3.
아니라면 병합. (em.merge(entity))
em = EntityManager
EntityManager는 엔티티를 관리하는 객체이다.
영속성 컨텍스트에 등록하거나 삭제 혹은 엔티티 상태를 DB에 저장한다.
id값을 받아오는데도 불구하고 수정이 아닌 생성이 되는 문제 발생.
문제
ArticleDTO.toEntity() 메소드에서 id 누락.
public Article toEntity() {
return new Article(title, content);
}
Java
복사
수정
public Article toEntity() {
return new Article(id, title, content);
}
Java
복사
DTO
DTO 바인딩 이슈
1.
@ModelAttribute
@PostMapping("/{id}/reply")
public String createReply(Model model, @PathVariable("id") Long id, ReplyDTO replyDTO) {
// ...
}
Java
복사
직접적으로 @ModelAttribute 어노테이션을 붙여주지 않았지만, ReplyDTO 앞에 생략되어 있는 것으로 볼 수 있다.
@ModelAttribute는 바인딩할 때 다음과 같은 규칙이 있다.
a.
@NoArgsConstructor와 @AllArgsConstructor 둘다 있는 경우.
@NoArgsConstructor 호출
setter로 필드 초기화
b.
@AllArgsConstructor만 있는 경우.
@AllArgsConstructor 호출하여 필드 초기화.
setter로 필드 다시 초기화
즉, @ModelAttribute로 DTO를 받아오는 경우 setter가 반드시 필요하다.
2.
@RequestBody
@PutMapping("/{articleId}/reply/{replyId}")
public String editReply(@PathVariable("replyId") Long replyId, @RequestBody ReplyDTO dto) {
// ...
}
Java
복사
Json 형식의 데이터를 받아올 때 @RequestBody 어노테이션을 사용한다.
Json을 DTO객체로 바인딩 할 때 리플렉션을 통해 객체를 구성한다.
따라서, setter 없이도 Json 데이터를 DTO로 바인딩할 수 있다. 하지만 리플렉션을 통해 바인딩하기 때문에 기본 생성자 (@NoArgsConstructor)가 반드시 필요하다.
댓글 기능 추가
Entity
Reply
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Reply {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String author;
private String content;
@ManyToOne
@JoinColumn(name = "article_id")
private Article article;
public Reply(Long id, String author, String content) {
this.id = id;
this.author = author;
this.content = content;
}
public void updateAuthor(String author) {
if (author != null) this.author = author;
}
public void updateContent(String content) {
if (content != null) this.content = content;
}
public void setArticle(Article article) {
this.article = article;
}
}
Java
복사
Article
@Entity
@Getter
@NoArgsConstructor
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@OneToMany(mappedBy = "article", fetch = FetchType.LAZY)
private List<Reply> replies = new ArrayList<>();
public Article(Long id, String title, String content) {
this.id = id;
this.title = title;
this.content = content;
}
public void addReply(Reply reply) { // 양방향 매핑시 각 객체에 서로를 설정해줘야한다.
reply.setArticle(this); // reply의 article을 설정
replies.add(reply); // article에 reply 추가
}
public void updateTitle(String title) {
if (title != null) this.title = title;
}
public void updateContent(String content) {
if (content != null) this.content = content;
}
}
Java
복사
연관관계 매핑
얼마 전 연관관계 매핑에 대해 공부하면서 정리했던 내용입니다.
영속성 전이
댓글이 달린 글을 삭제하면 글 삭제가 되지않는 문제 발생.
2022-11-10 13:58:30.760 ERROR 1 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement] with root cause
java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`likelion_board`.`reply`, CONSTRAINT `article_id` FOREIGN KEY (`article_id`) REFERENCES `article` (`id`))
Bash
복사
댓글이 달린 글을 삭제하면 댓글에 게시글 FK값은 존재하지만 게시글이 없는 문제 발생.
게시글을 삭제할때 연관관계를 맺고있는 댓글들도 모두 삭제되도록 영속성 전이 설정.
영속성 전이 설정 cascade = {CascadeType.REMOVE}
Article
@Entity
@Getter
@NoArgsConstructor
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@OneToMany(mappedBy = "article", fetch = FetchType.LAZY, cascade = {CascadeType.REMOVE})
private List<Reply> replies = new ArrayList<>();
// ...
}
Java
복사
정상 작동 확인!
Repository
ReplyRepository
public interface ReplyRepository extends JpaRepository<Reply, Long> {
}
Java
복사
Controller
ArticleController
@RequiredArgsConstructor
@Slf4j
@Controller
@RequestMapping("/articles")
public class ArticleController {
private final ArticleRepository articleRepository;
private final ReplyRepository replyRepository;
// 중략 ...
@PostMapping("/{id}/reply")
public String createReply(Model model, @PathVariable("id") Long id, ReplyDTO replyDTO) {
Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("해당 게시물을 찾을 수 없습니다."));
Reply reply = replyDTO.toEntity();
article.addReply(reply);
replyRepository.save(reply);
articleRepository.save(article);
model.addAttribute("article", article);
model.addAttribute("replies", article.getReplies());
return "articles/detail";
}
@DeleteMapping("/{articleId}/reply/{replyId}")
public String deleteReply(@PathVariable("replyId") Long replyId) {
replyRepository.deleteById(replyId);
return "articles/detail";
}
@PutMapping("/{articleId}/reply/{replyId}")
public String editReply(@PathVariable("replyId") Long replyId, @RequestBody ReplyDTO dto) {
log.info("id : {}, author : {}, content : {}", dto.getId(), dto.getAuthor(), dto.getContent());
Reply reply = replyRepository.findById(replyId).orElseThrow(() -> new RuntimeException("해당 댓글을 찾을 수 없습니다."));
reply.updateAuthor(dto.getAuthor());
reply.updateContent(dto.getContent());
// replyRepository.save(dto.toEntity()); // PK값으로 값이 있는지 검색, 이미 있으면 업데이트 없으면 인서트 : FK 때문에 실패
replyRepository.save(reply);
return "articles/detail";
}
}
Java
복사
Docker 배포
첫 배포 시
1. Dockerfile 작성
Dockerfile
docker 에서 이미지를 생성하기 위한 용도로 작성하는 파일
와일드 카드 적용
FROM gradle:7.4-jdk11-alpine as builder
WORKDIR /build
# 그래들 파일이 변경되었을 때만 새롭게 의존패키지 다운로드 받게함.
COPY build.gradle settings.gradle /build/
RUN gradle build -x test --parallel --continue > /dev/null 2>&1 || true
# 빌더 이미지에서 애플리케이션 빌드
COPY . /build
RUN gradle build -x test --parallel
# APP
FROM openjdk:11.0-slim
WORKDIR /app
# 빌더 이미지에서 jar 파일만 복사
COPY /build/build/libs/*-SNAPSHOT.jar ./app.jar
EXPOSE 8080
# root 대신 nobody 권한으로 실행
USER nobody
ENTRYPOINT [ \
"java", \
"-jar", \
"-Djava.security.egd=file:/dev/./urandom", \
"-Dsun.net.inetaddr.ttl=0", \
"app.jar" \
]
Docker
복사
학생 마다 프로젝트 명이 달라 빌드된 jar 파일 복사 명령어에 파일 이름을 수정해줘야하는 문제가 있었다.
와일드카드(*)를 적용하여 하나의 Dockerfile로 동작할 수 있도록 하였다.
와일드카드
파일을 지정할 때, 구체적인 이름 대신에 여러 파일을 동시에 지정할 목적으로 사용하는 특수 기호.
윈도우에서 *.jpg로 검색하면 파일 이름과 관계없이 jpg확장자를 가지고 있는 파일을 모두 검색할 수 있음.
즉, *-SNAPSHOT.jar는 파일이름이 -SANPSHOT.jar로 끝나는 파일을 모두 인식한다.
2. GitHub Push
배포할 프로젝트를 github에 push 한다.
이때 빌드를 위한 파일들을 누락하지 않고 push해야한다.
3. Git Clone
ec2에 접속하여 배포한 repository를 clone한다.
git clone {repository url}
Bash
복사
4. Docker Image Build
1.
프로젝트 폴더로 이동
cd {프로젝트 경로}
# cd : change directory 디렉토리 변경 리눅스 명령어
# cd .. : 상위 폴더로 이동
# cd ~ : 유저 폴더로 이동
Bash
복사
2.
이미지 빌드
sudo docker build -t {이미지 이름} .
# 마침표(.) 뒤에 띄어쓰기 잊지말기
Bash
복사
5. Docker Container Run
sudo docker run -d --name {컨테이너 이름} -p 8080:8080 \
-e SPRING_DATASOURCE_URL={DB host} \
-e SPRING_DATASOURCE_PASSWORD={DB Password} \
-e SPRING_DATASOURCE_USERNAME={DB user} \
{이미지 이름}
Bash
복사
변경 사항 적용 시
1. Docker Container Stop
sudo docker stop {컨테이너 이름 또는 컨테이너 아이디}
Bash
복사
기존 실행중이던 컨테이너 중지.
2. Docker Container Remove
sudo docker rm {컨테이너 이름 또는 컨테이너 아이디}
Bash
복사
컨테이너 삭제.
같은 이름의 컨테이너를 다시 올리기 위함.
이미지를 지우기 위함.
3. Docker Image Remove
sudo docker rmi {이미지 이름 또는 이미지 아이디}
Bash
복사
ec2 용량을 위해서라도 삭제
4. Git Pull
git pull
# 또는 git pull {remote name} {branch name}
Bash
복사
5. Docker Image Build
상동
6. Docker Container Run
상동
비정상일때 확인
1. Docker Process 확인
sudo docker ps -a
# sudo dokcer ps : 실행중인 프로세스만 출력
Bash
복사
STATUS
Exited : 프로세스 종료
Up * : 프로세스 실행 중
정상적으로 프로세스, 컨테이너가 실행중인지 확인해보세요.
2. Docker Container Log 확인
sudo docker logs {컨테이너 이름 또는 컨테이너 아이디}
Bash
복사
DB 관련 에러
•
HikariPool : ShutDown
•
CommunicationsException
•
…
1.
DB 정상 동작 확인
sudo docker ps -a
Bash
복사
mysql이 정상 동작 중인지 확인.
2.
도커 실행시 전달하는 환경변수(-e 옵션) 재확인.
환경변수를 정상적으로 전달하지 않아 DB에 연결하지 못하는 경우.
3.
AWS 인바운드 규칙 확인.
3306 포트가 막혀 DB에 연결하지 못하는 경우.
포트를 이미 사용 중인 경우
1.
명령어를 실행에 같은 포트를 사용중인 컨테이너가 있는지 확인.
sudo docker ps -a
Bash
복사
2.
같은 포트를 사용하고 있는 컨테이너가 존재하면 컨테이너 중지 및 삭제.
sudo docker stop {컨테이너 이름 또는 컨테이너 아이디}
Bash
복사
sudo docker rm {컨테이너 이름 또는 컨테이너 아이디}
Bash
복사