graph LR A["Client(Browser)"] -->|HTTP Request| B[@Controller] B --> C[@Service] C --> D[@Repository] D --> E[(Database)] E --> D D --> C C --> B B -->|HTTP Reponse| A
- spring은 이 구조를 정말 철저하게 지킴!
- controller, service, repository는 다 spring bean임
Diagram
graph LR subgraph User Application BR[Browser] JA[Java Application] end Service Layer subgraph Service Layer subgraph ProductService PS1["findAllProducts()"] PS2["findProductById()"] PS3["saveProduct()"] PS4["updateProduct()"] PS5["deleteProduct()"] end end Database subgraph Database DB1[(Product Table)] DB2[(User Table)] end %% 흐름 연결 BR --> PC1 BR --> PC2 BR --> PC3 BR --> PC4 BR --> PC5 JA --> PC1 JA --> PC3 PC1 --> PS1 --> PR1 --> DB1 PC2 --> PS2 --> PR2 --> DB1 PC3 --> PS3 --> PR3 --> DB1 PC4 --> PS4 --> PR3 --> DB1 PC5 --> PS5 --> PR4 --> DB1
Application Layers

| Layer | Key Responsibility | Description |
|---|---|---|
| Controller | Receives requests, returns responses | Controls the flow of processing by accepting client requests. - doesn’t know how the business logic works or how data is stored → just delegates - Contains ⭐Spring MVC |
| Service | Processes business logic | Handles core domain logic and manages transactions. |
| Repository | Accesses data | Only job is to communicate with the database (using JPA, MyBatis, etc.). - knows nothing about business logiv |
@Controller / @RestController
| Annotation | Description |
|---|---|
@Controller | Used in View-based MVC applications. - Typically paired with template engines like Thymeleaf (which is often the default choice)- When a method returns a String, it’s interpreted as a view name to render. |
@RestController | Designed for REST API responses. - Automatically includes @ResponseBody, meaning that whatever a method returns will be directly written into the HTTP response body (e.g., as JSON or XML), rather than being used to resolve a view.- @RestController = @Controller + @ResponseBody |
@ResponseBody- Java object → JSON, serializes return using Jackson
- Used in methods inside
@Controller
@RequestBody- JSON → Java object, deserializes using Jackson
- Used on method params for input parsing
@RestController // This implies @Controller + @ResponseBody
@RequestMapping("/hello")
public class HelloController {
@GetMapping // Handles GET requests to /hello
public String sayHello() {
// Returns the string "Hello, World!" directly as the HTTP response body
// Spring (via Jackson, implicitly) converts this to a JSON string if needed by the client.
return "Hello, World!";
}
}@Service
- The
@Serviceannotation explicitly marks a class as a service component. - It’s where you implement your application’s core business logic, such as handling transactions and enforcing domain-specific rules
- Use
@Transactionalannotation. Transactions are very important in backend applications (나중에 깊게 봄)- Uses AOP (Aspect Oriented Programming) internally
- Business logic = Our requirements of the application
- Use
- Depends on
repository
@Service
public class ProductService {
private final ProductRepository productRepository; // Injected by Spring
// Constructor for dependency injection
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// Method containing business logic
public List<Product> findAllProducts() {
return productRepository.findAll(); // Delegates to the repository for data access
}
}Tip
에러는 어디서 던져야 할까요? (Repository vs. Service) ⇒ SERVICE!
- Repository’s Job 💿: The repository’s only job is data access (e.g., find, save). It should return data or an empty state (like
Optional.empty()) to signal that data was not found. It shouldn’t make decisions or know business rules.- Service’s Job 🧠: The service implements business logic. It calls the repository to fetch data and then decides if the outcome violates a business rule. For example, if the service needs to update a user but the repository returns
Optional.empty(), the service throws aUserNotFoundExceptionbecause the business rule is “a user must exist to be updated.”
- Some services might allow creating users of the same name, some might not.
@Repository
- The
@Repositoryannotation marks a class as a Data Access Object (DAO), meaning it directly communicates with the database.- This is the only component that directly communicates with the db
- It essentially includes
@Componentinternally and provides automatic exception translation. This means database-specific exceptions (like aSQLException) are converted into Spring’s consistentDataAccessExceptionhierarchy, making your error handling more uniform - Commonly used with technology like Spring Data JPA (ORM 같은 기술 - 데이터를 객채로 나눔), MyBatis Mapper (SQL Mapper), etc
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// Spring Data JPA automatically provides basic methods like findById, findAll, save, etc.
}When NOT using technology like Spring JPA:
Tip
- Use
Optional<T>for methods that are expected to return at most one result.
- One item is found: The
Optionalwill contain the item.- No item is found: The
Optionalwill be empty (Optional.empty()), returningnull- In the service layer, handle
nullby throwing error if it receives null- Use a
Collection(likeList<T>orSet<T>) for methods that can return zero or more results.
- One or more items are found: The method returns a List containing those items.
- No items are found: The method returns an empty List. (not
null, but represents perfectly an empty list)- Avoid wrapping a collection in an
Optional(i.e.,Optional<List<T>>). This is considered an anti-pattern.
Examples of that:
@Override
public Optional<BinaryContent> findById(UUID id) {
BinaryContent binaryContentNullable = null;
Path path = resolvePath(id);
if (Files.exists(path)) {
try (
FileInputStream fis = new FileInputStream(path.toFile());
ObjectInputStream ois = new ObjectInputStream(fis);
) {
binaryContentNullable = (BinaryContent) ois.readObject();
} catch (IOException | ClassNotFoundException e){
throw new RuntimeException(e);
}
}
return Optional.ofNullable(binaryContentNullable);
}
@Override
public List<BinaryContent> findByUserId(UUID userId) {
try (Stream<Path> paths = Files.list(DIRECTORY)){
return paths
.filter(path -> path.toString().endsWith(EXTENSION))
// convert each object in path
.map(FileBinaryContentRepository::getBinaryContent)
.filter(b -> b.getUserId().equals(userId))
.toList();
} catch (IOException e) {
throw new RuntimeException(e);
}
}Dependency Relationships Between Layers
MUST follow a unidirectional dependencies between layers
Controller → Service → Repository
- Things you MUST KEEP
- You must avoid structures where the Repository refers to the Service, or the Service refers to the Controller.
- Such circular dependencies can lead to structural issues and become a source of errors during testing and maintenance.
| Layer | Depends On | Dependency Type |
|---|---|---|
| Controller | Service | Constructor Injection or Field Injection |
| Service | Repository | Constructor Injection |
| Repository | None (no lower layer) | N/A |
An incorrect dependency direction:
// BAD EXAMPLE: Avoid this!
@Repository
public class ProductRepository {
@Autowired
private ProductService productService; // Prohibited: Do not reference Service from Repository
}Dependencies Between Services in the Same Layer
- It’s actually common for one Service to depend on another Service.
- Ex) if you’re looking up order details and need information about the customer who placed the order, your
OrderServicemight need to callMemberService
@Service
public class OrderService {
private final MemberService memberService; // OrderService depends on MemberService
@Autowired
public OrderService(MemberService memberService) {
this.memberService = memberService;
}
/* Business logic to place an order */
public void placeOrder(Long memberId) {
// 1. Retrieve information for the currently logged-in user
// (ID provided as a parameter)
Member member = memberService.findById(memberId); // Calling MemberService
// 2. Order placement logic...
}
}- When you’re doing something like this, still keep these rules
- Unidirectional Flow (no circular dependencies, Spring will give u error)
- Clear responsibilities - Don’t let unrelated domains call each other directly—separate logic if needed
- Respect Hierarchy - E.g.,
OrderService → MemberServiceis OK, but reverse might indicate poor design.
- But what if we need to make
MemberServicealso depend onOrderService?- If
MemberServicealso depends onOrderService, there would be a circular dependency (순환참조), and Spring will not proceed & give an error. - Things you can do:
- Make
MemberServicedepend onOrderRepositoryinstead- But this is not recommended → tight coupling, hard to test/debug
MemberServicewill also have responsibilities ofOrderRepository, leading to unclear boundary of responsibilities- If possible, a layer should only depend on another domain of the same layer
- But this is not recommended → tight coupling, hard to test/debug
- Make one of those 2 an Event
- Make an intermediary class (explained below)
- Make
- If
Intermediary class
Alternatively, you can design a structure where multiple domains can cooperate through a dedicated component:
/* Example of separating a collaboration-specific component */
@Component
public class OrderProcessManager {
private final OrderService orderService;
private final MemberService memberService;
@Autowired
public OrderProcessManager(OrderService orderService, MemberService memberService) {
this.orderService = orderService;
this.memberService = memberService;
}
public void placeOrderForMember(Long memberId) {
Member member = memberService.findById(memberId);
orderService.createOrder(member);
}
}- This avoids direct mutual references between the two services by using
OrderProcessManager- Put the collaborative logic to a separate coordinator.
- The
OrderProcessManagerautomatically has its dependencies wired by Spring via constructor injection, and it internally still referencesOrderServiceandMemberService - The key is that the services aren’t directly coupled; they interact through an intermediary (the coordinator). This allows each service to focus solely on its own responsibilities.
Practical Design Tips
- Each layer should only depend on the layer(s) below it.
- For DI (Dependency Injection), primarily use Constructor Injection, implemented with Lombok’s
@RequiredArgsConstructoror Spring’s@Autowired. - When writing test code, clearly separating dependencies between layers makes Mocking and unit testing significantly easier.
- Slice Test: Tests a specific layer in isolation (e.g., testing only the
Controller). Dependent objects are replaced with fake objects (Mocking). - Unit Test: The smallest type of test, verifying that a single method works correctly.
- Slice Test: Tests a specific layer in isolation (e.g., testing only the