블로그
전체 52025. 03. 31.
0
[인프런 워밍업클럽 3기 백엔드 코드] 4주차 발자국
회고저번주에 강의를 다 듣고 이번주는 마지막 "테스트를 작성하는 마음가짐"이라는 마지막 강의로 강의를 마무리 하였다. 그리고 Spring REST Docs 강의만 다시 제대로 들어보고, 중간점검에서 코드리뷰를 받기전에 저번주에 했던 Day 11 미션 했던 것을 강의를 다 들은 입장에서 다시 보기도 했다. 뭔가 이것저것 고칠게 많을 거 같았는데 꼭 그렇지는 않더라. 중간점검이 끝나고 나서야 다른 사람들 꺼보고 이런저런 고쳐봐야겠다는 생각을 했다. 한 달간의 워밍클럽이 끝났는데 하던 코딩테스트, 알고리즘 공부를 조금 내려놓았어야 했다는 게 아쉬운 만큼 코딩테스트 공부만 했었다라면 접해보지 못했을 내용을 배울 수 있어서 뜻 깊었다. 스터디 공고 올라온거를 보면서도 내용이 너무 하고 싶었던 내용이라 연락을 드릴까 참 고민이 많았는데 원래 하려고 했던 코딩테스트 공부에 집중하고 먼저 끝내야겠다는 생각이 들어서 신청은 하지 않았다.또 좋았던 점은 직접 만나고 그러는 커뮤니티는 아니지만 질문이 생기면 질문할 수 있고 다른 사람들의 미션이나 발자국, 뭐 등등을 볼 수 있는 커뮤니티가 생겼다는 점도 큰 의미가 있는 것 같다. 지금까지 혼자 공부해왔기에 이런 부분에서 큰 갈증을 느끼고 있었기 때문에 더욱 필요했었다.미션 Day 16레이어 계층 구조에서 각 레이별의 특징과 테스트 방법에 대해서 적어보는 것이었다. 나름 레이어 계층 자체에 대해서 깊게 생각해 볼 수 있었다. 나름 계층 구조를 추상화해서 일상생활에서의 여러 서비스 형태도 나름의 비슷한 서비스, 컨트롤러, 리포지토리 계층 구조를 띠고 있다고 생각해 이를 토대로 설명을 해보았다. 미션 Day 18맨 처음에는 그냥 다 각각에 테스트에 적으면 되는 거 아닌가라는 생각을 했었는데, 현재 테스트에서 테스트하고자 하는 것이 무엇인지 파악해서 그것과 직접적으로 연관돼있는 것과 간접적으로 테스트하기 위해 필요한 것들을 구분할 필요가 있다는 것을 느끼고 그렇게 미션을 작성하였다.
2025. 03. 23.
0
[인프런 워밍업클럽 3기 백엔드 코드] 3주차 발자국
회고성격상 한번에 두 개 하는 걸 힘들어 하는 스타일이라 이번주는 나름의 결단으로 테스트 강의 듣는데에 거의 올인했다. (월요일 오전에만 좀 코테 공부함) 내가 하는 공부인 코테 공부랑 워밍업 클럽 둘 중 하나가 시간을 적게 써도 충분히 되는 거였다면 둘 다 병행을 해도 감당할 수 있었겠지만 워밍업 클럽을 진행해보니 이거에도 시간을 많이 써야겠다는 판단을 하게 됐다.그래서 맨 마지막 "테스트를 작성하는 마음가짐"이라는 마지막 강의를 제외하고서는 남은 모든 강의를 다 들었다.아직 뭔가 프로젝트 하나 해보지 못한 나는 당연히 테스트 코드를 스스로 짜본적이 없었고, 그래서 이 강의를 듣고 싶었는데, 다 듣고 나니까 추후에 프로젝트를 진행할 때, 어떤 식으로 테스트코드를 짜고 테스트코드를 바탕으로 프로덕션 코드를 짜야 될지에 대해 감을 잡을 수 있었다.다음주에는 복습하면서 이번주에 했던 11일차 미션했던 내용도 한번 배운 내용을 토대로 수정해봐야겠다.미션 Day 11https://github.com/myc0603/readable-code/tree/main/src/test/java/cleancode저번 리팩토링 강의를 통해 작성했던 지뢰찾기 코드와 스터디카페 코드 중 하나를 선택해서 단위테스트를 작성하는 것이 미션이었다.코드 자체가 스터디카페보다는 지뢰찾기도 좀 더 복잡했던 거 같아서 지뢰찾기를 선택해서 테스트 코드를 진행하였다.작성했던 클래스들과 메서드들을 하나하나 보면서 테스트해볼 수 있는 코드가 뭐가 있을까 보면서 테스트 코드를 작성해보았다. 하다보니 이걸 어떻게 테스트 해야되지? 라는 의문도 생기고 이것도 테스트를 해야되나? 라는 의문도 생기는 등 여러 의문이 생겼는데 강의를 듣다 보니 미션을 하면서 생겼던 의문 중 해소되었다.다음주에는 끝까지 들은 강의 내용을 바탕으로 테스트 코드를 다시 더 좋게 발전시켜볼 수 있을 거 같다.강의 내용Layer별 테스트Persistence Layer: 거의 단위테스트 느낌 얘만 써서 테스트 하면 됨Business Layer: Persistence Layer랑 같이 묶어서 통합 테스트 진행Presentation Layer: Business Layer, Persistence Layer를 Mocking해서 테스트 진행, 그냥 http요청을 보냈을 때 ok 응답이 오는 지 정도 확인 productNumber, 재고 등등.. -> 동시성 이슈 고려Transactional Transaction "readOnly" - CQRS : command(CUD)와 query(Read)를 분리서비스나 DB를 read용, write용 따로 만들 수 있음 JPA 사용시 객체를 캡쳐하는 등의 로직을 read만 할 때는 사용 안 할 수 있음테스트에서 사용하게 되면 프로덕션 코드에서 Transactional을 잘 사용했는 지에 대한 테스트가 안 되니 이점 유의해서 사용validation : 뭐에 대한 유효성 검증이냐에 따라 컨트롤러에서 검증할지 서비스에서 검증할지 등 고민해볼 필요가 있다. Mocking1. 가짜 객체를 만들어 사용 -> 필요한 경우 원하는 행위를 정해줄 수 있음 (Stubbing) 2. 원하는 메서드를 제외하고서는 진짜 객체처럼 행위를 원하면 Spy객체를 만들어 사용 3. Mocking을 어느 정도까지 할 지에 대해 고민해봐야 한다하나의 테스트는 하나만 검증!하지만 정말 하나에 테스트에서 딱 하나씩만 검증하다보면 테스트 별로 묶고 싶어지는 테스트가 생기기 마련=> @ParameterizedTest, @DynamicTest 사용!!완벽하게 제어하기컨트롤하지 못하는 값 사용을 지양, 외부 시스템 Mocking테스트 세팅 Test Fixture각 테스트에서 세팅 vs @BeforeEach/@BeforeAll 세팅을 하기위한 세팅 같은 너무 기본적인 세팅이면 @BeforeEach/@BeforeAll, 웬만하면 각 테스트 코드에서 세팅Test Fixture 클렌징deleteAll() vs deleteAllInBatch()테스트 환경 통합서버 띄우는 횟수를 줄여 테스트 코드 돌리는데 드는 시간 단축학습테스트학습에 테스트 활용Spring REST Docs이거 진짜 유용해보이는데 제대로 복습하고 써봐야겠다
백엔드
2025. 03. 16.
0
[인프런 워밍업클럽 3기 백엔드 코드] 2주차 발자국
회고 - 미션https://shore-judge-641.notion.site/Day7-1b34d1a0b2288042ad1fc21d2003a598?pvs=4아무래도 이번 일주일 중 가장 기억에 남는 것은 리팩토링 실습 미션이다. 코드 리뷰를 받는 거기도 해서 최대한 잘 해보고 싶다는 생각에 열심히 하다보니 하루를 통째로 써서 리팩토링을 하게 됐다.강의를 들을 때 강의 앞부분에서 어떤 내용을 가지고 리팩토링을 할지에 대해 설명을 들으면 혼자 한 번 생각나는 대로 코드도 좀 고쳐보고 그 다음에 강의를 듣던 식으로 해왔다. 그래서 그런식으로 생각나는 대로 하면 될 거 같았는데, 막상 혼자 해보려니까 쉽지 않았다.코드를 보면 뭐가 잘못 되긴 했구나라는 생각이 들고, 좀 더 보다보면 이게 문제니까 어떤 식으로 고쳐야겠다는 생각은 든다. 하지만 이 다음부터 고치려하면 어떻게 곷쳐나가야 하는지가 좀 막막했다.그럴때마다 하나의 리팩토링 과정에서는 하나만 진행해야 한다는 점을 마음속으로 되새기면서 하나씩 고쳐나가 보았다.리팩토링을 진행하면서 현재 고치고 있는 부분외에도 거슬리는 부분이 들어와 다른 거 막 고치고 싶은 생각이 들때도 있었는데 그럴때마다 일단 지금 하는 것부터 고치자며 마음을 다 잡고 진행했다.그리고 고치려는 게 너무 크게 느껴지면 조금씩 쪼개서 해보려고도 했다.리팩토링에서 뿐만 아니라 뭔가를 구현해야 할 때 문제를 풀어야 할 때 등 개발을 배우고 나서 많은 상황에서 뭔가를 한번에 하려고 하면 안되고 되는 것부터, 할 수 있겠다고 생각 드는 것부터 해나가야 되는구나를 여러번 느끼게 되는 거 같다. 중간 점검 라이브코드리뷰계속 원래 의도와 다르게 동작해서 나를 힘들게 해서 그 부분이 있어서 이에 대해 질문드렸었다.똑같은 내용을 복사해놓고 리팩토링을 진행하다보니 똑같은 이름의 다른 폴더에 있는 파일을 import를 해버려서 생긴 문제였다... 혹시 다른 폴더에 있는 걸 import하지 않았는지 확인을 하긴 했었는데 모든 파일을 확인했던게 아니라서 참..실전에서는 같은 프로젝트내에 같은 이름을 갖는 파일을 만들 것 같지는 않으니 괜찮을 거 같긴 하지만 가장 기억에 남는다..ㅎㅎ 코드리뷰를 보면서 다른 분들의 코드를 보고 다른 분들의 생각, 사고의 흐름을 볼 수 있었다.이때 코드 리뷰를 보면서 열린 마음으로 다른 사람의 코드를 볼 수 있어야 겠구나라는 생각을 했다. 우빈님은 본인의 원래 생각과 다르게 리팩토링을 한 코드를 보면서 충분한 이유가 있지 않을까 생각해보고 납득할 만한 이유가 있다는 생각이 들면 ok하셨다.뭐 나도 다른 분들 코드를 보면서 틀렸다고 생각한 적이 있는 건 아니지만, 아무래도 내가 리팩토링하면서 코드를 작성할 때 다 나름의 고민을 하고 그 고민의 결론에 맞게 코드를 짰기 때문에 나와 다른 결론을 가지고 짠 코드를 보면 바로 받아들여지지는 않는 것 같다. 나와 다른 코드를 받아들이기 위해서는 한 단계 받아들이기 위한 사고를 거쳐야 한달까?예를 들어 나는 A와 B중에 뭐가 더 나을까 고민해서 "음.. A가 좀 더 나은 듯!" 해서 A를 선택해서 코드를 작성했다면, B를 선택한 코드를 보면 바로 받아들여지기 보다는 "B보다는 A가 좀 더 낫다고 생각했는데?"라고 한 번 생각하고 나서 "음 다시 생각해보니까 A나 B나 뭐를 선택하든 뭐 비슷한가? 이 선택은 뭐를 확실히 더 낫다고 말하기는 애매한가?"라는 생각을 거쳐야 비로소 "B도 나쁘지 않군!", "B도 이렇게 하니까 괜찮네?"라는 결론을 내릴 수 있달까?의도하지 않으면 나와 다른 코드를 쉽게 배척해버릴 수도 있겠다는 생각을 해서 열린 마음이 필요하다는 생각을 하게 됐다.그리고 무엇보다 내가 고민조차 해보지 못한 지점에서 고민을 한 다른 분들의 코드를 보면서 저런 고민도 해볼 수 있겠다라는 생각을 했다. 강의내용 Readable Code강의 중에서 Minesweeper의 필드였던 gameStatus를 GameBoard의 필드로 옮긴 내용이 있었다.나는 처음에 이 부분이 바로 받아들여지지는 않았다. gameStatus라는 것은 말 그대로 Game에 대한 것이지 Board에 대한것이 아니라고 생각했기 때문이다. 그래서 어떤 근거로 gameStatus가 GameBoard의 필드가 될 수 있는 지에 대해 정리해 보았다.gameStatus의 상태를 바꾸거나 확인하는 것은 Minesweeper에 있었지만 이건 단지 현재 gameStatus가 Minesweeper의 필드로 있어서 그런 것일 뿐이다. gameStauts가 바뀌는 것은 GameBoard에서 선택된 셀에 어떤 act(flag 또는 open)를 할때이다. GameBoard에서 어떤 셀에 act를 할때 그대로 이어서 GameBoard에서 gameStatus를 바꾸든가 확인을 해야지 그걸 굳이 GameBoard의 메서드를 호출한 Minesweeper에게 다시 gameStatus에 대해 조작이나 확인을 부탁하는 것이 부자연스럽다. Minesweeper라는 이름때문에 게임자체를 의미하는 것 같지만 Minesweeper의 역할은 외부세계와의 접점이 되는 controller의 역할이다. 게임의 핵심적인 로직은 모두 GameBoard에 들어가있다. 그러니까 내가 생각한 Game 그 차제를 의미하는 거고 Minesweeper는 Game을 운영하는 것 뿐이다. 그래서 Game의 현재 이기고 지는 상태를 의미하는 gameStatus도 자연스레 GameBoard의 필드가 되어야 한다. DFS를 재귀로 구현했을 때의 stack overflow의 위험이 있다. -> 재귀 구현은 조심해서 해야 됨DFS가 안되면 BFS로 해야하나 싶긴 했는 데 둘 다 시간 공간 복잡도는 비슷한 걸로 기억한다.DFS를 메서드 재귀호출이 아닌 스택을 활용해서 구현해서 stack overflow를 방지한다. -> 스택을 활용해서 DFS를 구현할 수도 있다는 것을 배웠다. Practical Testing테스트를 통해 원하는 또는 필요한 기능을 정의한다. -> 테스트가 스펙이 된다. -> 테스트는 문서다.어떤 기능을 어떻게 사용하고 어떤 케이스에 어떤 결과가 나오는지에 대해 먼저 생각한다. -> 그렇다면 테스트를 먼저 작성해서 이로부터 기능을 구현해볼 수 있다. (TDD)
백엔드
2025. 03. 09.
0
[인프런 워밍업클럽 3기 백엔드 코드] 1주차 발자국
회고https://github.com/myc0603/MineSweeper지뢰찾기 코드를 가지고 리팩토링하는 방식으로 강의가 진행된다고 해서 강의 시작전에 혼자서 지뢰찾기 코드를 짜보았다. 강의를 들으면서 내 꺼에도 적용해볼랬는데, 시간이 모자라.. 다음에 시간내서 해야겠다.강의를 들으면서 어렴풋이 알고있는 추상화를 비롯한 여러 내용들이 어설프게 적용되어 있는 내 코드를 어떻게 발전시켜 나가야겠구나라는 생각을 할 수 있었다. 원래 하던 코딩테스트 알고리즘 공부를 하면서 같이 할 수 있을 거라 생각했는데 강의하나하나 오래 듣게 돼서 그런지 생각보다 시간을 많이 쓰게 된다. 자연스럽게 걍 빨리빨리 들어버릴까 싶기도 하고 코테공부도 빠르게빠르게 해볼까 싶어지는데 이럴때마다 속으로 급해지지 말자고 되새긴다. 아직 공부 시작한지 얼마 되지 않았고, 내 스타일대로 진득하게 공부해야 실력이 오르지, 급하게 쫓아가기만 해서는 안될 거 같다는 생각이다. 계속 꼼꼼히 하고, 공부시간을 최대한 더 늘려보자...!! 강의 내용 추상, 논리, 사고의 흐름이름 짓기 : 내가 너무 긴 이름을 사용하는 걸 지양하고 있었구나를 느꼈다. 이름이 너무 길면 코드가 좀 지저분해 보이기도 하고 가독성이 떨어지는 거 같은 느낌도 들었는데 강의가 진행되면서 내 코드의 가독성 문제는 다른 데 있었음을 알 수 있었다.메서드 추출, 추상화 레벨 : 내가 코드를 작성할 때의 가독성 문제가 바로 추상화 레벨에 대한 고려가 없었기 때문에 생겼다는 걸 알 수 있었다. 메서드 추출은 나름 가독성을 위해서 아니면 메서드 자체가 복잡해지지 않게 하고 나름 메서드를 봤을 때 어떤 역할을 하는지 알 수 있게 하기 위해 하긴 했는데 추상화 레벨을 고려하면서 한 메서드 안에서는 사용되는 로직, 메서드들의 추상화 레벨이 같아야 함을 알 수 있었다.뇌 메모리 적게 쓰기라는 강의에서 사실은 사람이 모든 걸 기억할 때 추상화해서 기억하고 정보를 저장한다는 것을 알았다. 이전에 사람이 망각하는 이유에 대해선 단순히 나쁜 기억을 뭐 오래 기억하지 않기 위해서? 뭐 이런식으로 알고 있었는데 이런 것도 뇌가 효율적으로 정보를 처리하고 사고도 같은 추상화레벨에서 효율적으로 사고하기 위해서구나를 느꼈다. 그래서 코드를 읽을 때에도 같은 추상화레벨의 내용이 계속 읽힐 수 있도록 신경써줘야 한다는 점을 느낄 수 있었다.예외처리 : 예외를 개발자가 애플리케이션 내에서 의도해서 사용하는 예외와 원하지 않는 예외를 구분해서 사용해야 한다는 것을 알았다. 의도해서 사용하는 예외는 자바에 원래 있던 예외와 구분하기 위해 새로 만들어서 사용하는 것이 좋다. 객체지향 패러다임, 적용setter를 사용하지 않아야 한다는 것은 알고 있었지만, getter도 사용에 주의해야 한다는 점에서 객체지향에서 유의해야 하는 것이 뭔지, 객체지향에서 각 클래스가 맡아야 하는 책임에 대해서 생각해 볼 수 있었다. getter로 얻은 데이터와 관련된 어떤 정보나 동작을 원할 때 그 정보나 동작을 해당 객체가 맡아서 할지 그 객체를 호출하는 client쪽에서 맡아야 할지에 대한 고려가 중요하다.SRP 책임에 대해서 잘 생각해 봐야 한다. 한 객체가 맡은 여러 행동(메서드)들이 과연 하나의 같은 책임에서 비롯된 것인지 분리될 수 있는 것인지 고려해봐야 한다.OCP 나중에 바뀔 수 있는 부분에 대해서 유연하게 동작할 수 있도록 코드를 작성한다. 예를 들어 DIP를 적용해서 사용하는 기능,모듈을 추상화해서 사용하는 것도 OCP원칙을 지키는 것이다.LSP 상속은 기능 확장이다. (자바의 예약어도 extends이다.) 기능 확장이라는 것은 원래의 기능을 바꾸는 것이 아니라 원래의 기능에 더해서 새로운 기능을 추가하는 것이다.ISP SRP의 인터페이스 버전. 어떤 인터페이스를 구현하려는 클래스에서 인터페이스에 선언된 모든 메서드를 선언할 필요가 없다면 해당 인터페이스는 하나의 책임이 아니라 둘 이상의 책임을 맡고 있는 것이다. 인터페이스가 하나의 책임만 맡도록 분리해야 한다.DIP 사용하는 기능과 모듈이 너무 저수준의 모듈이라서 변경가능성이 높다면 추상화해서 추후에 기능 확장 또는 변경에도 코드의 변경이 없도록 한다.Value Object : 변수에 이름만 짓는다고 그 값에 의미가 생기는 것이 아니다. 제대로 의미를 부여하고 싶다면 그 값을 감싸는 클래스를 만들어 원하는 기능을 만들어주거나, 유효성 검증 등을 해줄 수 있다. 객체로 감쌌기 때문에 equals() & hashCode(), final 필드 선언을 통해 동등성과 불변성을 갖도록 해준다.일급컬렉션 : 비즈니스 로직에서 어떤 컬렉션은 의미를 갖는다. 뭔소리냐면 전용 로직이 필요할 수 있단 소리. -> value object와 마찬가지로 감싸서 사용할 수 있다.ENUM : 구현체들을 커스터마이징해서 사용할일이 없거나 변경할 일이 없고, 상수로서만 사용할 때는 enum 타입 활용다형성 활용 : 강의 예제의 반복적인 if문 -> 추후에 추가적인 변경사항이 있을 때 많은 수정이 필요, 리팩토랭을 요함. 변하는 것과 변하지 않는 것을 확인. if문의 조건을 인터페이스화 시키고 각각의 조건을 구현체로 만든다. 조건에 해당하는 행위는 그 구현체의 메서드로 구현한다. 미션DAY2 : 추상화에 대해서 깊이 고민해볼 수 있는 시간을 가질 수 있었다. 일상의 일을 나름의 추상화 레벨를 나누어 구체적으로 설명해 보았다.DAY4 : 강의를 들으면서 노션에 적어놓은걸 정리해 미션을 완료했다. 다른 강의들을 때 지나가듯이 흘려 들었던 내용들이 촥 정리가 되었고, 그 이전 강의에서 어떤어떤 부분들이 SOLID의 어떤 원칙을 적용한거구나를 깨달을 수 있었다. 다음 미션이 그 다음 코드를 강의 듣기전에 미리 리팩토링 해보는 것 같던데 한번 기똥차게 리팩토링 해보고 싶다. 강의 : https://www.inflearn.com/course/readable-code-%EC%9D%BD%EA%B8%B0%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EC%82%AC%EA%B3%A0%EB%B2%95
백엔드
2025. 03. 07.
0
[인프런 워밍업 클럽 3기 BE 클린코드 & 테스트] - Day 4 미션 제출
1. public boolean validateOrder(Order order) { if (order.hasNoItems()) { log.info("주문 항목이 없습니다."); return false; } if (order.hasInvalidPrice()) { log.info("올바르지 않은 총 가격입니다."); return false; } if (order.hasNoCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } return true; } - SRP 단일책임원칙하나의 클래스는 하나의 책임만을 가지고 있어야 한다. 그래야 하나의 변경점을 가지고 코드를 수정해야 할 때 하나의 클래스만을 수정할 수 있다. → 높은 응집도와 낮은 결합도를 의미한다.결합도가 낮다는 것은 다시 말하면 서로 다른 클래스간 연관성이 낮고 하나의 클래스가 수정될 때 다른 클래스가 수정되지 않는 것을 의미한다.- OCP Open-Closed Principle추상화와 다형성을 활용해 코드의 수정 없이(Closed) 언제든 기능추가를 쉽게 할 수 있어야 한다(Open). 코드를 작성할 때 어떤 상수값이나 어떤 구체클래스에 의존해서 작성하면 상수값이 바뀌거나 구체클래스가 바뀌게 되면 이걸 사용하는 코드가 다 변경되어야 한다. 사용하는 값이나 클래스가 바뀌더라도 유연하게 사용할 수 있도록 코드를 작성해야 된다.- LSP부모 클래스로 선언된 변수가 자식클래스의 인스턴스든, 부모클래스의 인스턴스든 같은 역할을 하고 같은 행동을 해야 한다. 다르게 말하면 어떤 클래스를 상속받았으면 그 클래스의 특징을 그대로 받아야 하고 말그대로 원래의 기능에다가 확장할 뿐이다. 원래의 기능을 변경하면 안된다.구체적인 상황에서 보면 부모클래스의 인스턴스를 자식클래스의 인스턴스가 치환되더라도 부모클래스 인스턴스의 행동과 다르지 않아야 한다.- ISP인터페이스의 기능 인터페이스를 구현하는 클래스의 기능보다 인터페이스의 기능이 더 많아서 구현하는 클래스에서 다 구현하지 못한다면 인터페이스를 쪼개야 한다. 약간 인터페이스의 SRP(단일책임원칙)? 같은 느낌. 너무 많은 기능(책임)이 있으면 쪼개라! 구현하는 클래스 입장에서는 구현해야되는 인터페이스의 수의 변화만 있을뿐이다.- DIP추상화레벨이 높은 모듈,클래스 등에서 추상화 레벨이 낮은 모듈,클래스를 의존해야 할 때 의존 대상을 추상화해서 의존해야 한다. (구체클레스보다는 이를 추상화한 인터페이스를 의존). 추상화 레벨이 낮을 수록 변경 가능성이 높기 때문에 추상화 하지 않은 저수준 모듈을 직접 의존하게 된다면 저수준 모듈에 변경이 있을 때 고수준 모듈에도 변경이 필요하게 된다.
백엔드